server_test.go 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912
  1. package mcp
  2. import (
  3. "context"
  4. "io"
  5. "testing"
  6. "time"
  7. "github.com/opencost/opencost/core/pkg/clustercache"
  8. "github.com/opencost/opencost/core/pkg/opencost"
  9. models "github.com/opencost/opencost/pkg/cloud/models"
  10. "github.com/opencost/opencost/pkg/cloudcost"
  11. "github.com/opencost/opencost/pkg/costmodel"
  12. "github.com/stretchr/testify/assert"
  13. "github.com/stretchr/testify/require"
  14. )
  15. func TestQueryTypeConstants(t *testing.T) {
  16. assert.Equal(t, QueryType("allocation"), AllocationQueryType)
  17. assert.Equal(t, QueryType("asset"), AssetQueryType)
  18. assert.Equal(t, QueryType("cloudcost"), CloudCostQueryType)
  19. }
  20. func TestAllocationQueryStruct(t *testing.T) {
  21. query := AllocationQuery{
  22. Step: 1 * time.Hour,
  23. Accumulate: true,
  24. ShareIdle: true,
  25. Aggregate: "namespace",
  26. IncludeIdle: true,
  27. IdleByNode: true,
  28. IncludeProportionalAssetResourceCosts: true,
  29. IncludeAggregatedMetadata: true,
  30. ShareLB: true,
  31. }
  32. assert.Equal(t, 1*time.Hour, query.Step)
  33. assert.True(t, query.Accumulate)
  34. assert.True(t, query.ShareIdle)
  35. assert.Equal(t, "namespace", query.Aggregate)
  36. assert.True(t, query.IncludeIdle)
  37. assert.True(t, query.IdleByNode)
  38. assert.True(t, query.IncludeProportionalAssetResourceCosts)
  39. assert.True(t, query.IncludeAggregatedMetadata)
  40. assert.True(t, query.ShareLB)
  41. }
  42. func TestAssetQueryStruct(t *testing.T) {
  43. query := AssetQuery{}
  44. // AssetQuery is currently empty, just test that it can be created
  45. assert.NotNil(t, query)
  46. }
  47. func TestCloudCostQueryStruct(t *testing.T) {
  48. query := CloudCostQuery{
  49. Aggregate: "provider,service",
  50. Accumulate: "day",
  51. Filter: "provider=aws",
  52. Provider: "aws",
  53. Service: "ec2",
  54. Category: "compute",
  55. Region: "us-east-1",
  56. AccountID: "123456789",
  57. }
  58. assert.Equal(t, "provider,service", query.Aggregate)
  59. assert.Equal(t, "day", query.Accumulate)
  60. assert.Equal(t, "provider=aws", query.Filter)
  61. assert.Equal(t, "aws", query.Provider)
  62. assert.Equal(t, "ec2", query.Service)
  63. assert.Equal(t, "compute", query.Category)
  64. assert.Equal(t, "us-east-1", query.Region)
  65. assert.Equal(t, "123456789", query.AccountID)
  66. }
  67. func TestMCPRequestStruct(t *testing.T) {
  68. request := MCPRequest{
  69. SessionID: "test-session-123",
  70. Query: &OpenCostQueryRequest{
  71. QueryType: AllocationQueryType,
  72. Window: "24h",
  73. AllocationParams: &AllocationQuery{
  74. Step: 1 * time.Hour,
  75. Accumulate: true,
  76. ShareIdle: true,
  77. },
  78. },
  79. }
  80. assert.Equal(t, "test-session-123", request.SessionID)
  81. assert.NotNil(t, request.Query)
  82. assert.Equal(t, AllocationQueryType, request.Query.QueryType)
  83. assert.Equal(t, "24h", request.Query.Window)
  84. assert.NotNil(t, request.Query.AllocationParams)
  85. assert.Equal(t, 1*time.Hour, request.Query.AllocationParams.Step)
  86. assert.True(t, request.Query.AllocationParams.Accumulate)
  87. assert.True(t, request.Query.AllocationParams.ShareIdle)
  88. }
  89. func TestMCPResponseStruct(t *testing.T) {
  90. response := MCPResponse{
  91. Data: "test-data",
  92. QueryInfo: QueryMetadata{
  93. QueryID: "query-123",
  94. Timestamp: time.Now(),
  95. ProcessingTime: 100 * time.Millisecond,
  96. },
  97. }
  98. assert.Equal(t, "test-data", response.Data)
  99. assert.Equal(t, "query-123", response.QueryInfo.QueryID)
  100. assert.NotZero(t, response.QueryInfo.Timestamp)
  101. assert.Equal(t, 100*time.Millisecond, response.QueryInfo.ProcessingTime)
  102. }
  103. func TestQueryMetadataStruct(t *testing.T) {
  104. metadata := QueryMetadata{
  105. QueryID: "query-456",
  106. Timestamp: time.Now(),
  107. ProcessingTime: 250 * time.Millisecond,
  108. }
  109. assert.Equal(t, "query-456", metadata.QueryID)
  110. assert.NotZero(t, metadata.Timestamp)
  111. assert.Equal(t, 250*time.Millisecond, metadata.ProcessingTime)
  112. }
  113. func TestOpenCostQueryRequestStruct(t *testing.T) {
  114. request := OpenCostQueryRequest{
  115. QueryType: AssetQueryType,
  116. Window: "7d",
  117. AssetParams: &AssetQuery{},
  118. }
  119. assert.Equal(t, AssetQueryType, request.QueryType)
  120. assert.Equal(t, "7d", request.Window)
  121. assert.NotNil(t, request.AssetParams)
  122. }
  123. // Test helper functions
  124. func createTestAllocation(name string) *Allocation {
  125. now := time.Now()
  126. return &Allocation{
  127. Name: name,
  128. CPUCost: 10.0,
  129. RAMCost: 5.0,
  130. GPUCost: 0.0,
  131. PVCost: 2.0,
  132. NetworkCost: 1.0,
  133. SharedCost: 0.5,
  134. ExternalCost: 0.0,
  135. TotalCost: 18.5,
  136. CPUCoreHours: 100.0,
  137. RAMByteHours: 5000000000.0,
  138. GPUHours: 0.0,
  139. PVByteHours: 2000000000.0,
  140. Start: now.Add(-24 * time.Hour),
  141. End: now,
  142. }
  143. }
  144. func createTestAsset(name string) *Asset {
  145. now := time.Now()
  146. return &Asset{
  147. Type: "node",
  148. Properties: AssetProperties{
  149. Category: "compute",
  150. Provider: "aws",
  151. Name: name,
  152. },
  153. CPUCost: 50.0,
  154. RAMCost: 25.0,
  155. GPUCost: 100.0,
  156. TotalCost: 175.0,
  157. CPUCoreHours: 500.0,
  158. RAMByteHours: 25000000000.0,
  159. GPUHours: 50.0,
  160. Start: now.Add(-24 * time.Hour),
  161. End: now,
  162. }
  163. }
  164. func createTestCloudCost(name string) *CloudCost {
  165. now := time.Now()
  166. return &CloudCost{
  167. Properties: CloudCostProperties{
  168. Provider: "aws",
  169. Service: "ec2",
  170. },
  171. Window: TimeWindow{
  172. Start: now.Add(-24 * time.Hour),
  173. End: now,
  174. },
  175. ListCost: CostMetric{
  176. Cost: 100.0,
  177. KubernetesPercent: 80.0,
  178. },
  179. NetCost: CostMetric{
  180. Cost: 95.0,
  181. KubernetesPercent: 80.0,
  182. },
  183. }
  184. }
  185. // Test MCP server response structures
  186. func TestAllocationResponseStruct(t *testing.T) {
  187. allocation := createTestAllocation("test-namespace")
  188. allocationSet := &AllocationSet{
  189. Name: "test-namespace",
  190. Properties: map[string]string{
  191. "namespace": "test-namespace",
  192. },
  193. Allocations: []*Allocation{allocation},
  194. }
  195. response := AllocationResponse{
  196. Allocations: map[string]*AllocationSet{
  197. "test-namespace": allocationSet,
  198. },
  199. }
  200. require.NotNil(t, response.Allocations)
  201. assert.Len(t, response.Allocations, 1)
  202. assert.Contains(t, response.Allocations, "test-namespace")
  203. allocSet := response.Allocations["test-namespace"]
  204. assert.Equal(t, "test-namespace", allocSet.Name)
  205. assert.Len(t, allocSet.Allocations, 1)
  206. alloc := allocSet.Allocations[0]
  207. assert.Equal(t, "test-namespace", alloc.Name)
  208. assert.Equal(t, 10.0, alloc.CPUCost)
  209. assert.Equal(t, 5.0, alloc.RAMCost)
  210. assert.Equal(t, 18.5, alloc.TotalCost)
  211. }
  212. func TestAssetResponseStruct(t *testing.T) {
  213. asset := createTestAsset("test-node")
  214. assetSet := &AssetSet{
  215. Name: "test-node",
  216. Assets: []*Asset{asset},
  217. }
  218. response := AssetResponse{
  219. Assets: map[string]*AssetSet{
  220. "test-node": assetSet,
  221. },
  222. }
  223. require.NotNil(t, response.Assets)
  224. assert.Len(t, response.Assets, 1)
  225. assert.Contains(t, response.Assets, "test-node")
  226. assetSetResult := response.Assets["test-node"]
  227. assert.Equal(t, "test-node", assetSetResult.Name)
  228. assert.Len(t, assetSetResult.Assets, 1)
  229. assetResult := assetSetResult.Assets[0]
  230. assert.Equal(t, "node", assetResult.Type)
  231. assert.Equal(t, 50.0, assetResult.CPUCost)
  232. assert.Equal(t, 25.0, assetResult.RAMCost)
  233. assert.Equal(t, 100.0, assetResult.GPUCost)
  234. assert.Equal(t, 175.0, assetResult.TotalCost)
  235. }
  236. func TestCloudCostResponseStruct(t *testing.T) {
  237. cloudCost := createTestCloudCost("aws-ec2")
  238. cloudCostSet := &CloudCostSet{
  239. Name: "aws-ec2",
  240. CloudCosts: []*CloudCost{cloudCost},
  241. Window: &TimeWindow{
  242. Start: time.Now().Add(-24 * time.Hour),
  243. End: time.Now(),
  244. },
  245. }
  246. response := CloudCostResponse{
  247. CloudCosts: map[string]*CloudCostSet{
  248. "aws-ec2": cloudCostSet,
  249. },
  250. Summary: &CloudCostSummary{
  251. TotalNetCost: 95.0,
  252. TotalAmortizedCost: 90.0,
  253. TotalInvoicedCost: 100.0,
  254. KubernetesPercent: 80.0,
  255. },
  256. }
  257. require.NotNil(t, response.CloudCosts)
  258. assert.Len(t, response.CloudCosts, 1)
  259. assert.Contains(t, response.CloudCosts, "aws-ec2")
  260. costSet := response.CloudCosts["aws-ec2"]
  261. assert.Equal(t, "aws-ec2", costSet.Name)
  262. assert.Len(t, costSet.CloudCosts, 1)
  263. cost := costSet.CloudCosts[0]
  264. assert.Equal(t, "aws", cost.Properties.Provider)
  265. assert.Equal(t, "ec2", cost.Properties.Service)
  266. assert.Equal(t, 100.0, cost.ListCost.Cost)
  267. assert.Equal(t, 95.0, cost.NetCost.Cost)
  268. require.NotNil(t, response.Summary)
  269. assert.Equal(t, 95.0, response.Summary.TotalNetCost)
  270. assert.Equal(t, 80.0, response.Summary.KubernetesPercent)
  271. }
  272. // Test allocation set functionality
  273. func TestAllocationSetTotalCost(t *testing.T) {
  274. alloc1 := createTestAllocation("alloc1")
  275. alloc1.TotalCost = 10.0
  276. alloc2 := createTestAllocation("alloc2")
  277. alloc2.TotalCost = 15.0
  278. allocSet := &AllocationSet{
  279. Name: "test-set",
  280. Allocations: []*Allocation{alloc1, alloc2},
  281. }
  282. totalCost := allocSet.TotalCost()
  283. assert.Equal(t, 25.0, totalCost)
  284. }
  285. // Test asset properties
  286. func TestAssetProperties(t *testing.T) {
  287. props := AssetProperties{
  288. Category: "compute",
  289. Provider: "aws",
  290. Account: "123456789",
  291. Project: "my-project",
  292. Service: "ec2",
  293. Cluster: "prod-cluster",
  294. Name: "worker-node-1",
  295. ProviderID: "i-1234567890abcdef0",
  296. }
  297. assert.Equal(t, "compute", props.Category)
  298. assert.Equal(t, "aws", props.Provider)
  299. assert.Equal(t, "123456789", props.Account)
  300. assert.Equal(t, "my-project", props.Project)
  301. assert.Equal(t, "ec2", props.Service)
  302. assert.Equal(t, "prod-cluster", props.Cluster)
  303. assert.Equal(t, "worker-node-1", props.Name)
  304. assert.Equal(t, "i-1234567890abcdef0", props.ProviderID)
  305. }
  306. // Test cloud cost properties
  307. func TestCloudCostProperties(t *testing.T) {
  308. props := CloudCostProperties{
  309. ProviderID: "i-1234567890abcdef0",
  310. Provider: "aws",
  311. AccountID: "123456789",
  312. AccountName: "my-account",
  313. InvoiceEntityID: "entity-123",
  314. InvoiceEntityName: "My Company",
  315. RegionID: "us-east-1",
  316. AvailabilityZone: "us-east-1a",
  317. Service: "ec2",
  318. Category: "compute",
  319. Labels: map[string]string{
  320. "environment": "production",
  321. "team": "platform",
  322. },
  323. }
  324. assert.Equal(t, "i-1234567890abcdef0", props.ProviderID)
  325. assert.Equal(t, "aws", props.Provider)
  326. assert.Equal(t, "123456789", props.AccountID)
  327. assert.Equal(t, "my-account", props.AccountName)
  328. assert.Equal(t, "entity-123", props.InvoiceEntityID)
  329. assert.Equal(t, "My Company", props.InvoiceEntityName)
  330. assert.Equal(t, "us-east-1", props.RegionID)
  331. assert.Equal(t, "us-east-1a", props.AvailabilityZone)
  332. assert.Equal(t, "ec2", props.Service)
  333. assert.Equal(t, "compute", props.Category)
  334. assert.Equal(t, "production", props.Labels["environment"])
  335. assert.Equal(t, "platform", props.Labels["team"])
  336. }
  337. // Test cost metric
  338. func TestCostMetric(t *testing.T) {
  339. metric := CostMetric{
  340. Cost: 100.0,
  341. KubernetesPercent: 80.0,
  342. }
  343. assert.Equal(t, 100.0, metric.Cost)
  344. assert.Equal(t, 80.0, metric.KubernetesPercent)
  345. }
  346. // Test time window
  347. func TestTimeWindow(t *testing.T) {
  348. now := time.Now()
  349. window := TimeWindow{
  350. Start: now.Add(-24 * time.Hour),
  351. End: now,
  352. }
  353. assert.True(t, window.Start.Before(window.End))
  354. assert.Equal(t, 24*time.Hour, window.End.Sub(window.Start))
  355. }
  356. // Test node overhead
  357. func TestNodeOverhead(t *testing.T) {
  358. overhead := NodeOverhead{
  359. RamOverheadFraction: 0.1,
  360. CpuOverheadFraction: 0.05,
  361. OverheadCostFraction: 0.15,
  362. }
  363. assert.Equal(t, 0.1, overhead.RamOverheadFraction)
  364. assert.Equal(t, 0.05, overhead.CpuOverheadFraction)
  365. assert.Equal(t, 0.15, overhead.OverheadCostFraction)
  366. }
  367. // Test asset breakdown
  368. func TestAssetBreakdown(t *testing.T) {
  369. breakdown := AssetBreakdown{
  370. Idle: 10.0,
  371. Other: 5.0,
  372. System: 15.0,
  373. User: 70.0,
  374. }
  375. assert.Equal(t, 10.0, breakdown.Idle)
  376. assert.Equal(t, 5.0, breakdown.Other)
  377. assert.Equal(t, 15.0, breakdown.System)
  378. assert.Equal(t, 70.0, breakdown.User)
  379. }
  380. // Test cloud cost summary
  381. func TestCloudCostSummary(t *testing.T) {
  382. summary := CloudCostSummary{
  383. TotalNetCost: 1000.0,
  384. TotalAmortizedCost: 950.0,
  385. TotalInvoicedCost: 1100.0,
  386. KubernetesPercent: 85.0,
  387. ProviderBreakdown: map[string]float64{
  388. "aws": 800.0,
  389. "gcp": 200.0,
  390. },
  391. ServiceBreakdown: map[string]float64{
  392. "ec2": 600.0,
  393. "s3": 200.0,
  394. "rds": 200.0,
  395. },
  396. RegionBreakdown: map[string]float64{
  397. "us-east-1": 600.0,
  398. "us-west-2": 400.0,
  399. },
  400. }
  401. assert.Equal(t, 1000.0, summary.TotalNetCost)
  402. assert.Equal(t, 950.0, summary.TotalAmortizedCost)
  403. assert.Equal(t, 1100.0, summary.TotalInvoicedCost)
  404. assert.Equal(t, 85.0, summary.KubernetesPercent)
  405. assert.Equal(t, 800.0, summary.ProviderBreakdown["aws"])
  406. assert.Equal(t, 200.0, summary.ProviderBreakdown["gcp"])
  407. assert.Equal(t, 600.0, summary.ServiceBreakdown["ec2"])
  408. assert.Equal(t, 200.0, summary.ServiceBreakdown["s3"])
  409. assert.Equal(t, 600.0, summary.RegionBreakdown["us-east-1"])
  410. assert.Equal(t, 400.0, summary.RegionBreakdown["us-west-2"])
  411. }
  412. // Test default values
  413. func TestAllocationQueryDefaultValues(t *testing.T) {
  414. query := AllocationQuery{}
  415. // Test default values
  416. assert.Equal(t, time.Duration(0), query.Step)
  417. assert.False(t, query.Accumulate)
  418. assert.False(t, query.ShareIdle)
  419. assert.Empty(t, query.Aggregate)
  420. assert.False(t, query.IncludeIdle)
  421. assert.False(t, query.IdleByNode)
  422. assert.False(t, query.IncludeProportionalAssetResourceCosts)
  423. assert.False(t, query.IncludeAggregatedMetadata)
  424. assert.False(t, query.ShareLB)
  425. }
  426. func TestCloudCostQueryDefaultValues(t *testing.T) {
  427. query := CloudCostQuery{}
  428. // Test default values
  429. assert.Empty(t, query.Aggregate)
  430. assert.Empty(t, query.Accumulate)
  431. assert.Empty(t, query.Filter)
  432. assert.Empty(t, query.Provider)
  433. assert.Empty(t, query.Service)
  434. assert.Empty(t, query.Category)
  435. assert.Empty(t, query.Region)
  436. assert.Empty(t, query.AccountID)
  437. }
  438. // Test edge cases
  439. func TestEdgeCases(t *testing.T) {
  440. t.Run("zero duration step", func(t *testing.T) {
  441. query := AllocationQuery{
  442. Step: 0,
  443. }
  444. assert.Equal(t, time.Duration(0), query.Step)
  445. })
  446. t.Run("negative duration step", func(t *testing.T) {
  447. query := AllocationQuery{
  448. Step: -1 * time.Hour,
  449. }
  450. assert.Equal(t, -1*time.Hour, query.Step)
  451. })
  452. t.Run("very large duration step", func(t *testing.T) {
  453. query := AllocationQuery{
  454. Step: 365 * 24 * time.Hour, // 1 year
  455. }
  456. assert.Equal(t, 365*24*time.Hour, query.Step)
  457. })
  458. t.Run("empty aggregate string", func(t *testing.T) {
  459. query := AllocationQuery{
  460. Aggregate: "",
  461. }
  462. assert.Empty(t, query.Aggregate)
  463. })
  464. t.Run("comma separated aggregate", func(t *testing.T) {
  465. query := AllocationQuery{
  466. Aggregate: "namespace,cluster,node",
  467. }
  468. assert.Equal(t, "namespace,cluster,node", query.Aggregate)
  469. })
  470. }
  471. // dummyQuerier captures the last QueryRequest it received
  472. type dummyQuerier struct {
  473. last cloudcost.QueryRequest
  474. }
  475. func (dq *dummyQuerier) Query(_ context.Context, req cloudcost.QueryRequest) (*opencost.CloudCostSetRange, error) {
  476. dq.last = req
  477. // Return empty set range
  478. ccsr, _ := opencost.NewCloudCostSetRange(time.Now().Add(-24*time.Hour), time.Now(), opencost.AccumulateOptionDay, "")
  479. return ccsr, nil
  480. }
  481. func TestBuildCloudCostQueryRequest_AccumulateParsing(t *testing.T) {
  482. s := &MCPServer{}
  483. req := cloudcost.QueryRequest{}
  484. params := &CloudCostQuery{
  485. Aggregate: "provider,service",
  486. Accumulate: "week",
  487. }
  488. out := s.buildCloudCostQueryRequest(req, params)
  489. assert.Equal(t, []string{"provider", "service"}, out.AggregateBy)
  490. assert.NotEqual(t, opencost.AccumulateOptionNone, out.Accumulate)
  491. }
  492. func TestBuildCloudCostQueryRequest_FilterString(t *testing.T) {
  493. s := &MCPServer{}
  494. req := cloudcost.QueryRequest{}
  495. params := &CloudCostQuery{
  496. Filter: `provider:"gcp" and service:"Compute Engine"`,
  497. }
  498. out := s.buildCloudCostQueryRequest(req, params)
  499. assert.NotNil(t, out.Filter)
  500. }
  501. func TestBuildFilterFromParams_SupportedFieldsOnly(t *testing.T) {
  502. s := &MCPServer{}
  503. params := &CloudCostQuery{
  504. Provider: "gcp",
  505. ProviderID: "cluster-1",
  506. Service: "Compute Engine",
  507. Category: "compute",
  508. AccountID: "acct-123",
  509. InvoiceEntityID: "inv-456",
  510. Region: "us-central1", // intentionally set; ignored by builder
  511. Labels: map[string]string{
  512. "goog-k8s-cluster-name": "cluster-1",
  513. },
  514. }
  515. f := s.buildFilterFromParams(params)
  516. assert.NotNil(t, f)
  517. }
  518. func TestBuildFilterFromParams_LabelOnly(t *testing.T) {
  519. s := &MCPServer{}
  520. params := &CloudCostQuery{
  521. Labels: map[string]string{"environment": "prod"},
  522. }
  523. f := s.buildFilterFromParams(params)
  524. assert.NotNil(t, f)
  525. }
  526. func TestQueryCloudCosts_QuerierCapture(t *testing.T) {
  527. dq := &dummyQuerier{}
  528. s := &MCPServer{cloudQuerier: dq}
  529. req := &OpenCostQueryRequest{
  530. QueryType: CloudCostQueryType,
  531. Window: "5d",
  532. CloudCostParams: &CloudCostQuery{
  533. Aggregate: "provider,service",
  534. Accumulate: "week",
  535. Provider: "gcp",
  536. },
  537. }
  538. _, err := s.QueryCloudCosts(req)
  539. require.NoError(t, err)
  540. assert.Equal(t, []string{"provider", "service"}, dq.last.AggregateBy)
  541. assert.NotEqual(t, opencost.AccumulateOptionNone, dq.last.Accumulate)
  542. }
  543. // ---- Tests for MCP server end-to-end behavior ----
  544. func TestProcessMCPRequest_CloudCostDispatch(t *testing.T) {
  545. dq := &dummyQuerier{}
  546. s := &MCPServer{cloudQuerier: dq}
  547. req := &MCPRequest{
  548. Query: &OpenCostQueryRequest{
  549. QueryType: CloudCostQueryType,
  550. Window: "3d",
  551. CloudCostParams: &CloudCostQuery{
  552. Aggregate: "provider",
  553. Accumulate: "day",
  554. Provider: "gcp",
  555. },
  556. },
  557. }
  558. resp, err := s.ProcessMCPRequest(req)
  559. require.NoError(t, err)
  560. require.NotNil(t, resp)
  561. require.NotNil(t, resp.Data)
  562. }
  563. func TestProcessMCPRequest_UnsupportedType(t *testing.T) {
  564. s := &MCPServer{}
  565. req := &MCPRequest{
  566. Query: &OpenCostQueryRequest{
  567. QueryType: QueryType("unknown"),
  568. Window: "1d",
  569. },
  570. }
  571. _, err := s.ProcessMCPRequest(req)
  572. require.Error(t, err)
  573. }
  574. func TestProcessMCPRequest_ValidationError(t *testing.T) {
  575. s := &MCPServer{}
  576. // Missing window
  577. req := &MCPRequest{
  578. Query: &OpenCostQueryRequest{
  579. QueryType: CloudCostQueryType,
  580. Window: "",
  581. },
  582. }
  583. _, err := s.ProcessMCPRequest(req)
  584. require.Error(t, err)
  585. }
  586. // ---- Additional comprehensive tests for missing functionality ----
  587. func TestNewMCPServer(t *testing.T) {
  588. costModel := &costmodel.CostModel{}
  589. provider := &mockProvider{}
  590. cloudQuerier := &dummyQuerier{}
  591. server := NewMCPServer(costModel, provider, cloudQuerier)
  592. require.NotNil(t, server)
  593. assert.Equal(t, costModel, server.costModel)
  594. assert.Equal(t, provider, server.provider)
  595. assert.Equal(t, cloudQuerier, server.cloudQuerier)
  596. }
  597. // Mock provider for testing
  598. type mockProvider struct{}
  599. func (mp *mockProvider) GetConfig() (*models.CustomPricing, error) { return nil, nil }
  600. func (mp *mockProvider) AllNodePricing() (interface{}, error) { return nil, nil }
  601. func (mp *mockProvider) ClusterInfo() (map[string]string, error) { return nil, nil }
  602. func (mp *mockProvider) GetAddresses() ([]byte, error) { return nil, nil }
  603. func (mp *mockProvider) GetDisks() ([]byte, error) { return nil, nil }
  604. func (mp *mockProvider) GetOrphanedResources() ([]models.OrphanedResource, error) { return nil, nil }
  605. func (mp *mockProvider) NodePricing(models.Key) (*models.Node, models.PricingMetadata, error) {
  606. return nil, models.PricingMetadata{}, nil
  607. }
  608. func (mp *mockProvider) GpuPricing(map[string]string) (string, error) { return "", nil }
  609. func (mp *mockProvider) PVPricing(models.PVKey) (*models.PV, error) { return nil, nil }
  610. func (mp *mockProvider) NetworkPricing() (*models.Network, error) { return nil, nil }
  611. func (mp *mockProvider) LoadBalancerPricing() (*models.LoadBalancer, error) { return nil, nil }
  612. func (mp *mockProvider) DownloadPricingData() error { return nil }
  613. func (mp *mockProvider) GetKey(map[string]string, *clustercache.Node) models.Key { return nil }
  614. func (mp *mockProvider) GetPVKey(*clustercache.PersistentVolume, map[string]string, string) models.PVKey {
  615. return nil
  616. }
  617. func (mp *mockProvider) UpdateConfig(io.Reader, string) (*models.CustomPricing, error) {
  618. return nil, nil
  619. }
  620. func (mp *mockProvider) UpdateConfigFromConfigMap(map[string]string) (*models.CustomPricing, error) {
  621. return nil, nil
  622. }
  623. func (mp *mockProvider) GetManagementPlatform() (string, error) { return "", nil }
  624. func (mp *mockProvider) ApplyReservedInstancePricing(map[string]*models.Node) {}
  625. func (mp *mockProvider) ServiceAccountStatus() *models.ServiceAccountStatus { return nil }
  626. func (mp *mockProvider) PricingSourceStatus() map[string]*models.PricingSource { return nil }
  627. func (mp *mockProvider) ClusterManagementPricing() (string, float64, error) { return "", 0, nil }
  628. func (mp *mockProvider) CombinedDiscountForNode(string, bool, float64, float64) float64 { return 0 }
  629. func (mp *mockProvider) Regions() []string { return nil }
  630. func (mp *mockProvider) PricingSourceSummary() interface{} { return nil }
  631. func TestGenerateQueryID(t *testing.T) {
  632. // Test that generateQueryID returns a non-empty string
  633. id1 := generateQueryID()
  634. id2 := generateQueryID()
  635. assert.NotEmpty(t, id1)
  636. assert.NotEmpty(t, id2)
  637. assert.NotEqual(t, id1, id2) // Should be different each time
  638. assert.Contains(t, id1, "query-")
  639. }
  640. func TestTransformAllocationSet_NilInput(t *testing.T) {
  641. result := transformAllocationSet(nil)
  642. require.NotNil(t, result)
  643. assert.NotNil(t, result.Allocations)
  644. assert.Len(t, result.Allocations, 0)
  645. }
  646. func TestTransformAllocationSet_EmptyInput(t *testing.T) {
  647. emptySet := &opencost.AllocationSet{
  648. Allocations: map[string]*opencost.Allocation{},
  649. }
  650. result := transformAllocationSet(emptySet)
  651. require.NotNil(t, result)
  652. assert.Contains(t, result.Allocations, "allocations")
  653. assert.Len(t, result.Allocations["allocations"].Allocations, 0)
  654. }
  655. func TestTransformAssetSet_NilInput(t *testing.T) {
  656. result := transformAssetSet(nil)
  657. require.NotNil(t, result)
  658. assert.NotNil(t, result.Assets)
  659. assert.Len(t, result.Assets, 0)
  660. }
  661. func TestTransformAssetSet_EmptyInput(t *testing.T) {
  662. emptySet := &opencost.AssetSet{
  663. Assets: map[string]opencost.Asset{},
  664. }
  665. result := transformAssetSet(emptySet)
  666. require.NotNil(t, result)
  667. assert.Contains(t, result.Assets, "assets")
  668. assert.Len(t, result.Assets["assets"].Assets, 0)
  669. }
  670. func TestBuildFilterFromParams_EmptyParams(t *testing.T) {
  671. s := &MCPServer{}
  672. params := &CloudCostQuery{}
  673. filter := s.buildFilterFromParams(params)
  674. assert.Nil(t, filter)
  675. }
  676. func TestBuildFilterFromParams_RegionIgnored(t *testing.T) {
  677. s := &MCPServer{}
  678. params := &CloudCostQuery{
  679. Region: "us-east-1", // Should be ignored
  680. }
  681. filter := s.buildFilterFromParams(params)
  682. assert.Nil(t, filter) // Should return nil since only region is set
  683. }
  684. func TestBuildFilterFromParams_EmptyLabelKey(t *testing.T) {
  685. s := &MCPServer{}
  686. params := &CloudCostQuery{
  687. Labels: map[string]string{
  688. "": "value1", // Empty key should be ignored
  689. "valid": "value2",
  690. },
  691. }
  692. filter := s.buildFilterFromParams(params)
  693. assert.NotNil(t, filter)
  694. }
  695. func TestBuildCloudCostQueryRequest_EmptyParams(t *testing.T) {
  696. s := &MCPServer{}
  697. req := cloudcost.QueryRequest{}
  698. params := &CloudCostQuery{}
  699. result := s.buildCloudCostQueryRequest(req, params)
  700. assert.Equal(t, req, result) // Should return unchanged request
  701. }
  702. func TestBuildCloudCostQueryRequest_InvalidFilterString(t *testing.T) {
  703. s := &MCPServer{}
  704. req := cloudcost.QueryRequest{}
  705. params := &CloudCostQuery{
  706. Filter: "invalid filter syntax !!!",
  707. }
  708. result := s.buildCloudCostQueryRequest(req, params)
  709. // Should not panic and should return request with nil filter
  710. assert.Nil(t, result.Filter)
  711. }
  712. func TestQueryCloudCosts_NilCloudQuerier(t *testing.T) {
  713. s := &MCPServer{cloudQuerier: nil}
  714. req := &OpenCostQueryRequest{
  715. QueryType: CloudCostQueryType,
  716. Window: "24h",
  717. }
  718. _, err := s.QueryCloudCosts(req)
  719. require.Error(t, err)
  720. assert.Contains(t, err.Error(), "cloud cost querier not configured")
  721. }
  722. func TestQueryCloudCosts_InvalidWindow(t *testing.T) {
  723. s := &MCPServer{cloudQuerier: &dummyQuerier{}}
  724. req := &OpenCostQueryRequest{
  725. QueryType: CloudCostQueryType,
  726. Window: "invalid-window",
  727. }
  728. _, err := s.QueryCloudCosts(req)
  729. require.Error(t, err)
  730. assert.Contains(t, err.Error(), "failed to parse window")
  731. }
  732. func TestQueryAssets_InvalidWindow(t *testing.T) {
  733. s := &MCPServer{}
  734. req := &OpenCostQueryRequest{
  735. QueryType: AssetQueryType,
  736. Window: "invalid-window",
  737. }
  738. _, err := s.QueryAssets(req)
  739. require.Error(t, err)
  740. assert.Contains(t, err.Error(), "failed to parse window")
  741. }
  742. func TestQueryAllocations_InvalidWindow(t *testing.T) {
  743. s := &MCPServer{}
  744. req := &OpenCostQueryRequest{
  745. QueryType: AllocationQueryType,
  746. Window: "invalid-window",
  747. }
  748. _, err := s.QueryAllocations(req)
  749. require.Error(t, err)
  750. assert.Contains(t, err.Error(), "failed to parse window")
  751. }
  752. func TestProcessMCPRequest_ResponseMetadata(t *testing.T) {
  753. dq := &dummyQuerier{}
  754. s := &MCPServer{cloudQuerier: dq}
  755. req := &MCPRequest{
  756. Query: &OpenCostQueryRequest{
  757. QueryType: CloudCostQueryType,
  758. Window: "1h",
  759. },
  760. }
  761. resp, err := s.ProcessMCPRequest(req)
  762. require.NoError(t, err)
  763. require.NotNil(t, resp)
  764. // Check response metadata
  765. assert.NotEmpty(t, resp.QueryInfo.QueryID)
  766. assert.NotZero(t, resp.QueryInfo.Timestamp)
  767. assert.Greater(t, resp.QueryInfo.ProcessingTime, time.Duration(0))
  768. }
  769. func TestCloudCostQuery_NewFields(t *testing.T) {
  770. query := CloudCostQuery{
  771. InvoiceEntityID: "entity-123",
  772. ProviderID: "provider-456",
  773. Labels: map[string]string{
  774. "environment": "prod",
  775. "team": "platform",
  776. },
  777. }
  778. assert.Equal(t, "entity-123", query.InvoiceEntityID)
  779. assert.Equal(t, "provider-456", query.ProviderID)
  780. assert.Equal(t, "prod", query.Labels["environment"])
  781. assert.Equal(t, "platform", query.Labels["team"])
  782. }