diff --git a/src/component-library/Pages/Helpers/Heading.stories.tsx b/src/component-library/Pages/Helpers/Heading.stories.tsx index df092a3d8..eaa69a6f7 100644 --- a/src/component-library/Pages/Helpers/Heading.stories.tsx +++ b/src/component-library/Pages/Helpers/Heading.stories.tsx @@ -1,7 +1,6 @@ -import { DIDType, LockState } from "@/lib/core/entity/rucio"; +import { DIDType } from "@/lib/core/entity/rucio"; import { StoryFn, Meta } from "@storybook/react"; import { DIDTypeTag } from "../../Tags/DIDTypeTag"; -import { LockStateTag } from "../../Tags/LockStateTag"; import { Heading as H } from "./Heading"; export default { diff --git a/src/component-library/Tags/LockStateTag.tsx b/src/component-library/Tags/LockStateTag.tsx index 257a81e92..ef0f08db4 100644 --- a/src/component-library/Tags/LockStateTag.tsx +++ b/src/component-library/Tags/LockStateTag.tsx @@ -22,8 +22,8 @@ export const LockStateTag: ( + + /** + * Lists all rules for a given account. + * @param rucioAuthToken A valid Rucio Auth Token. + */ + listRules(rucioAuthToken: string): Promise + + /** + * Lists all locks for a given rule. + * @param rucioAuthToken A valid Rucio Auth Token. + * @param ruleId The rule to list locks for. + */ + listRuleReplicaLockStates(rucioAuthToken: string, ruleId: string): Promise + +} \ No newline at end of file diff --git a/src/lib/infrastructure/gateway/rule-gateway/endpoints/get-rule-endpoints.ts b/src/lib/infrastructure/gateway/rule-gateway/endpoints/get-rule-endpoints.ts new file mode 100644 index 000000000..c8370c2bd --- /dev/null +++ b/src/lib/infrastructure/gateway/rule-gateway/endpoints/get-rule-endpoints.ts @@ -0,0 +1,59 @@ +import { RuleDTO } from "@/lib/core/dto/rule-dto"; +import { BaseEndpoint } from "@/lib/sdk/gateway-endpoints"; +import { HTTPRequest } from "@/lib/sdk/http"; +import { convertToRuleDTO, getEmptyRuleDTO, TRucioRule } from "../rule-gateway-utils"; +import { Response } from "node-fetch"; + +export default class GetRuleEndpoint extends BaseEndpoint { + constructor( + private readonly rucioAuthToken: string, + private readonly ruleId: string, + ){ + super() + } + + async initialize(): Promise { + await super.initialize() + const rucioHost = await this.envConfigGateway.rucioHost() + const endpoint = `${rucioHost}/rules/${this.ruleId}` + const request: HTTPRequest = { + method: 'GET', + url: endpoint, + headers: { + 'X-Rucio-Auth-Token': this.rucioAuthToken, + 'Content-Type': 'application/json', + }, + } + this.request = request + this.initialized = true + } + + /** + * If this method is called, it means that the response from Rucio was not in any of the error types in ${@link handleCommonGatewayEndpointErrors} + * @param statusCode The status code returned from Rucio + * @param response The reponse containing error data + * @returns + */ + async reportErrors(statusCode: number, response: Response): Promise { + const data = await response.json() + const errorDTO: RuleDTO = { + ...getEmptyRuleDTO(), + status: 'error', + errorMessage: data, + errorCode: statusCode, + errorName: 'Unknown Error', + errorType: 'gateway-endpoint-error', + } + return Promise.resolve(errorDTO) + } + + /** + * Converts stream elements into RSEDTO objects + * @param response The individual RSE object streamed from Rucio + * @returns The RSEDTO object + */ + createDTO(data: TRucioRule): RuleDTO { + const dto: RuleDTO = convertToRuleDTO(data) + return dto + } +} \ No newline at end of file diff --git a/src/lib/infrastructure/gateway/rule-gateway/endpoints/list-rule-replica-lock-states-endpoint.ts b/src/lib/infrastructure/gateway/rule-gateway/endpoints/list-rule-replica-lock-states-endpoint.ts new file mode 100644 index 000000000..829a6fcce --- /dev/null +++ b/src/lib/infrastructure/gateway/rule-gateway/endpoints/list-rule-replica-lock-states-endpoint.ts @@ -0,0 +1,61 @@ +import { RuleReplicaLockStateDTO } from "@/lib/core/dto/rule-dto" +import { BaseStreamableDTO } from "@/lib/sdk/dto" +import { BaseStreamableEndpoint } from "@/lib/sdk/gateway-endpoints" +import { HTTPRequest } from "@/lib/sdk/http" +import { Response } from "node-fetch" +import { convertToRuleReplicaLockDTO, TRucioRuleReplicaLock } from "../rule-gateway-utils" +export default class ListRuleReplicaLockStatesEndpoint extends BaseStreamableEndpoint { + constructor( + private readonly rucioAuthToken: string, + private readonly ruleId: string, + ){ + super(true) + } + + async initialize(): Promise { + await super.initialize() + const rucioHost = await this.envConfigGateway.rucioHost() + const endpoint = `${rucioHost}/rules/${this.ruleId}/locks` + const request: HTTPRequest = { + method: 'GET', + url: endpoint, + headers: { + 'X-Rucio-Auth-Token': this.rucioAuthToken, + 'Content-Type': 'application/x-json-stream', + }, + } + this.request = request + this.initialized = true + } + + /** + * If this method is called, it means that the response from Rucio was not in any of the error types in ${@link handleCommonGatewayEndpointErrors} + * @param statusCode The status code returned from Rucio + * @param response The reponse containing error data + * @returns + */ + async reportErrors(statusCode: number, response: Response): Promise { + const data = await response.json() + const errorDTO: BaseStreamableDTO = { + status: 'error', + errorMessage: data, + errorCode: statusCode, + errorName: 'Unknown Error', + errorType: 'gateway-endpoint-error', + stream: null, + } + return Promise.resolve(errorDTO) + + } + + /** + * Converts stream elements into RSEDTO objects + * @param response The individual RSE object streamed from Rucio + * @returns The RSEDTO object + */ + createDTO(response: Buffer): RuleReplicaLockStateDTO { + const data: TRucioRuleReplicaLock = JSON.parse(JSON.parse(response.toString())) + const dto: RuleReplicaLockStateDTO = convertToRuleReplicaLockDTO(data) + return dto + } +} \ No newline at end of file diff --git a/src/lib/infrastructure/gateway/rule-gateway/endpoints/list-rules-for-account-endpoint.ts b/src/lib/infrastructure/gateway/rule-gateway/endpoints/list-rules-for-account-endpoint.ts new file mode 100644 index 000000000..6b92bb1da --- /dev/null +++ b/src/lib/infrastructure/gateway/rule-gateway/endpoints/list-rules-for-account-endpoint.ts @@ -0,0 +1,67 @@ +import { RuleDTO } from "@/lib/core/dto/rule-dto"; +import { BaseStreamableDTO } from "@/lib/sdk/dto"; +import { BaseStreamableEndpoint } from "@/lib/sdk/gateway-endpoints"; +import { HTTPRequest } from "@/lib/sdk/http"; +import { Response } from "node-fetch"; +import { convertToRuleDTO, TRucioRule } from "../rule-gateway-utils"; + + +/** + * Endpoint for listing rules for an account + */ +export default class ListRulesEndpoint extends BaseStreamableEndpoint { + constructor( + private readonly rucioAuthToken: string, + ){ + super(true) + } + + async initialize(): Promise { + await super.initialize() + const rucioHost = await this.envConfigGateway.rucioHost() + const endpoint = `${rucioHost}/rules/` + const request: HTTPRequest = { + method: 'GET', + url: endpoint, + headers: { + 'X-Rucio-Auth-Token': this.rucioAuthToken, + 'Content-Type': 'application/x-json-stream', + }, + } + this.request = request + this.initialized = true + } + + /** + * If this method is called, it means that the response from Rucio was not in any of the error types in ${@link handleCommonGatewayEndpointErrors} + * @param statusCode The status code returned from Rucio + * @param response The reponse containing error data + * @returns + */ + async reportErrors(statusCode: number, response: Response): Promise { + const data = await response.json() + const errorDTO: BaseStreamableDTO = { + status: 'error', + errorMessage: data, + errorCode: statusCode, + errorName: 'Unknown Error', + errorType: 'gateway-endpoint-error', + stream: null, + } + return Promise.resolve(errorDTO) + + } + + /** + * Converts stream elements into RSEDTO objects + * @param response The individual RSE object streamed from Rucio + * @returns The RSEDTO object + */ + + createDTO(response: Buffer): RuleDTO { + const data: TRucioRule = JSON.parse(JSON.parse(response.toString())) + const dto: RuleDTO = convertToRuleDTO(data) + return dto + } + +} \ No newline at end of file diff --git a/src/lib/infrastructure/gateway/rule-gateway/rule-gateway-utils.ts b/src/lib/infrastructure/gateway/rule-gateway/rule-gateway-utils.ts new file mode 100644 index 000000000..65fffb1f2 --- /dev/null +++ b/src/lib/infrastructure/gateway/rule-gateway/rule-gateway-utils.ts @@ -0,0 +1,132 @@ +import { RuleDTO, RuleReplicaLockStateDTO } from "@/lib/core/dto/rule-dto"; +import { LockState, RuleState } from "@/lib/core/entity/rucio"; + +export type TRucioRule = { + error: null | string; + locks_stuck_cnt: number; + ignore_availability: boolean; + meta: null | string; + subscription_id: null | string; + rse_expression: string; + source_replica_expression: null | string; + ignore_account_limit: boolean; + created_at: string; + account: string; + copies: number; + activity: string; + priority: number; + updated_at: string; + scope: string; + expires_at: null | string; + grouping: string; + name: string; + weight: null | number; + notification: string; + comments: null | string; + did_type: string; + locked: boolean; + stuck_at: null | string; + child_rule_id: null | string; + state: string; + locks_ok_cnt: number; + purge_replicas: boolean; + eol_at: null | string; + id: string; + locks_replicating_cnt: number; + split_container: boolean; + bytes: null | number; +}; + +export type TRucioRuleReplicaLock = { + scope: string; + name: string; + rse_id: string; + rse: string; + state: string; + rule_id: string; +} + +function getRuleState(state: string): RuleState { + const cleanState = state.trim().toUpperCase() + switch(cleanState) { + case 'REPLICATING': + return RuleState.REPLICATING + case 'OK': + return RuleState.OK + case 'STUCK': + return RuleState.STUCK + case 'SUSPENDED': + return RuleState.SUSPENDED + case 'WAITING_APPROVAL': + return RuleState.WAITING_APPROVAL + case 'INJECT': + return RuleState.INJECT + default: + return RuleState.UNKNOWN + } +} + +function getReplicaLockState(state: string): LockState { + const cleanState = state.trim().toUpperCase() + switch(cleanState) { + case 'REPLICATING': + return LockState.REPLICATING + case 'OK': + return LockState.OK + case 'STUCK': + return LockState.STUCK + default: + return LockState.UNKNOWN + } +} + +export function convertToRuleDTO(rule: TRucioRule): RuleDTO { + return { + status: 'success', + id: rule.id, + name: rule.name, + account: rule.account, + rse_expression: rule.rse_expression, + created_at: rule.created_at, + remaining_lifetime: rule.expires_at ? new Date(rule.expires_at).getTime() - Date.now() : 0, + state: getRuleState(rule.state), + locks_ok_cnt: rule.locks_ok_cnt, + locks_replicating_cnt: rule.locks_replicating_cnt, + locks_stuck_cnt: rule.locks_stuck_cnt, + } +} + +export function convertToRuleReplicaLockDTO(ruleReplicaLockState: TRucioRuleReplicaLock): RuleReplicaLockStateDTO { + return { + status: 'success', + scope: ruleReplicaLockState.scope, + name: ruleReplicaLockState.name, + rse: ruleReplicaLockState.rse, + state: getReplicaLockState(ruleReplicaLockState.state), + } +} +export function getEmptyRuleDTO(): RuleDTO { + return { + status: 'error', + id: '', + name: '', + account: '', + rse_expression: '', + created_at: '', + remaining_lifetime: 0, + state: RuleState.UNKNOWN, + locks_ok_cnt: 0, + locks_replicating_cnt: 0, + locks_stuck_cnt: 0, + } +} + +export function getEmptyRuleReplicaLockDTO(): RuleReplicaLockStateDTO { + return { + status: 'error', + scope: '', + name: '', + rse: '', + state: LockState.UNKNOWN, + } +} \ No newline at end of file diff --git a/src/lib/infrastructure/gateway/rule-gateway/rule-gateway.ts b/src/lib/infrastructure/gateway/rule-gateway/rule-gateway.ts new file mode 100644 index 000000000..94d19f13c --- /dev/null +++ b/src/lib/infrastructure/gateway/rule-gateway/rule-gateway.ts @@ -0,0 +1,62 @@ +import { RuleDTO } from "@/lib/core/dto/rule-dto"; +import RuleGatewayOutputPort from "@/lib/core/port/secondary/rule-gateway-output-port"; +import { BaseStreamableDTO } from "@/lib/sdk/dto"; +import { injectable } from "inversify"; +import GetRuleEndpoint from "./endpoints/get-rule-endpoints"; +import ListRuleReplicaLockStatesEndpoint from "./endpoints/list-rule-replica-lock-states-endpoint"; +import ListRulesEndpoint from "./endpoints/list-rules-for-account-endpoint"; + +@injectable() +export default class RuleGateway implements RuleGatewayOutputPort { + async getRule(rucioAuthToken: string, ruleId: string): Promise { + const endpoint = new GetRuleEndpoint(rucioAuthToken, ruleId) + const dto = await endpoint.fetch() + return dto + } + + async listRules(rucioAuthToken: string): Promise { + try { + const endpoint = new ListRulesEndpoint(rucioAuthToken) + const errorDTO: BaseStreamableDTO | undefined = await endpoint.fetch() + if(!errorDTO) { + return { + status: 'success', + stream: endpoint + } + } + return errorDTO + }catch(error) { + const errorDTO: BaseStreamableDTO = { + status: 'error', + errorName: 'Exception occurred while fetching rules', + errorType: 'gateway_endpoint_error', + errorMessage: error?.toString(), + } + return Promise.resolve(errorDTO) + } + } + + + async listRuleReplicaLockStates(rucioAuthToken: string, ruleId: string): Promise { + try { + const endpoint = new ListRuleReplicaLockStatesEndpoint(rucioAuthToken, ruleId) + const errorDTO: BaseStreamableDTO | undefined = await endpoint.fetch() + if(!errorDTO) { + return { + status: 'success', + stream: endpoint + } + } + return errorDTO + } catch(error) { + const errorDTO: BaseStreamableDTO = { + status: 'error', + errorName: 'Exception occurred while fetching rule replica locks', + errorType: 'gateway_endpoint_error', + errorMessage: error?.toString(), + } + return Promise.resolve(errorDTO) + } + } + +} \ No newline at end of file diff --git a/src/lib/infrastructure/ioc/container-config.ts b/src/lib/infrastructure/ioc/container-config.ts index fae56c23a..fc3e1a061 100644 --- a/src/lib/infrastructure/ioc/container-config.ts +++ b/src/lib/infrastructure/ioc/container-config.ts @@ -61,6 +61,8 @@ import ListRSEsFeature from "./features/list-rses-feature"; import ListAllRSEsFeature from "./features/list-all-rses-feature"; import GetSiteHeaderFeature from "./features/get-site-header-feature"; import ListSubscriptionRuleStatesFeature from "./features/list-subscription-rule-states-feature"; +import RuleGatewayOutputPort from "@/lib/core/port/secondary/rule-gateway-output-port"; +import RuleGateway from "../gateway/rule-gateway/rule-gateway"; /** @@ -75,7 +77,7 @@ appContainer.bind(GATEWAYS.RSE).to(RSEGateway); appContainer.bind(GATEWAYS.STREAM).to(StreamingGateway).inRequestScope(); appContainer.bind(GATEWAYS.SUBSCRIPTION).to(SubscriptionGateway); appContainer.bind(GATEWAYS.REPLICA).to(ReplicaGateway); - +appContainer.bind(GATEWAYS.RULE).to(RuleGateway); // Load Common Features loadFeaturesSync(appContainer, [ diff --git a/src/lib/infrastructure/ioc/ioc-symbols-gateway.ts b/src/lib/infrastructure/ioc/ioc-symbols-gateway.ts index c76ae0b70..5b25327a6 100644 --- a/src/lib/infrastructure/ioc/ioc-symbols-gateway.ts +++ b/src/lib/infrastructure/ioc/ioc-symbols-gateway.ts @@ -10,6 +10,7 @@ const GATEWAYS = { STREAM: Symbol.for("StreamGateway"), SUBSCRIPTION: Symbol.for("SubscriptionGateway"), REPLICA: Symbol.for("ReplicaGateway"), + RULE: Symbol.for("RuleGateway"), } export default GATEWAYS; diff --git a/src/pages/api/feature/mock-update-subscription.ts b/src/pages/api/feature/mock-update-subscription.ts index c6ee1cf30..f8c3a34cf 100644 --- a/src/pages/api/feature/mock-update-subscription.ts +++ b/src/pages/api/feature/mock-update-subscription.ts @@ -1,7 +1,6 @@ import { withAuthenticatedSessionRoute } from "@/lib/infrastructure/auth/session-utils"; import { NextApiRequest, NextApiResponse } from "next"; import { fixtureSubscriptionViewModel } from "test/fixtures/table-fixtures"; -import { Readable } from "stream"; async function endpoint(req: NextApiRequest, res: NextApiResponse, rucioAuthToken: string) { if(req.method !== 'PUT') { diff --git a/test/fixtures/table-fixtures.ts b/test/fixtures/table-fixtures.ts index e1e5e038e..330460517 100644 --- a/test/fixtures/table-fixtures.ts +++ b/test/fixtures/table-fixtures.ts @@ -92,7 +92,7 @@ export function fixtureRulePageLockEntryViewModel(): RulePageLockEntryViewModel scope: createRandomScope(), name: faker.string.alphanumeric(10), rse: createRSEName(), - state: faker.helpers.arrayElement(['R', 'O', 'S']) as LockState, + state: faker.helpers.arrayElement(['R', 'O', 'S', 'U']) as LockState, ddm_link: faker.internet.url(), fts_link: faker.internet.url(), } diff --git a/test/gateway/rule/rule-gateway-get-rule.test.ts b/test/gateway/rule/rule-gateway-get-rule.test.ts new file mode 100644 index 000000000..5d601ba03 --- /dev/null +++ b/test/gateway/rule/rule-gateway-get-rule.test.ts @@ -0,0 +1,131 @@ +import { RuleDTO, RuleReplicaLockStateDTO } from "@/lib/core/dto/rule-dto"; +import { LockState, RuleState } from "@/lib/core/entity/rucio"; +import RuleGatewayOutputPort from "@/lib/core/port/secondary/rule-gateway-output-port"; +import appContainer from "@/lib/infrastructure/ioc/container-config"; +import GATEWAYS from "@/lib/infrastructure/ioc/ioc-symbols-gateway"; +import { BaseStreamableDTO } from "@/lib/sdk/dto"; +import MockRucioServerFactory, { MockEndpoint } from "test/fixtures/rucio-server"; +import { collectStreamedData } from "test/fixtures/stream-test-utils"; +import { Readable } from "stream"; + + +describe("Rule Gateway", () => { + beforeEach(() => { + fetchMock.doMock(); + const getRuleEndpoint: MockEndpoint = { + url: `${MockRucioServerFactory.RUCIO_HOST}/rules/817b3030097446a38b3b842bf528e112`, + method: 'GET', + endsWith: '817b3030097446a38b3b842bf528e112', + response: { + status: 200, + headers: { + 'Content-Type': 'application/x-json-stream', + }, + body: JSON.stringify({ + "error": null, + "locks_stuck_cnt": 0, + "ignore_availability": false, + "meta": null, + "subscription_id": null, + "rse_expression": "XRD3", + "source_replica_expression": null, + "ignore_account_limit": false, + "created_at": "Mon, 27 Nov 2023 17:57:44 UTC", + "account": "root", + "copies": 1, + "activity": "User Subscriptions", + "priority": 3, + "updated_at": "Mon, 27 Nov 2023 17:57:44 UTC", + "scope": "test", + "expires_at": null, + "grouping": "DATASET", + "name": "container", + "weight": null, + "notification": "NO", + "comments": null, + "did_type": "CONTAINER", + "locked": false, + "stuck_at": null, + "child_rule_id": null, + "state": "REPLICATING", + "locks_ok_cnt": 0, + "purge_replicas": false, + "eol_at": null, + "id": "817b3030097446a38b3b842bf528e112", + "locks_replicating_cnt": 4, + "split_container": false + }) + } + } + + const replicaLockStates = [ + JSON.stringify({ + "scope": "test", + "name": "file1", + "rse_id": "c8b8113ddcdb4ec78e0846171e594280", + "rse": "XRD3", + "state": "REPLICATING", + "rule_id": "817b3030097446a38b3b842bf528e112" + }), + JSON.stringify({ + "scope": "test", + "name": "file2", + "rse_id": "c8b8113ddcdb4ec78e0846171e594280", + "rse": "XRD3", + "state": "REPLICATING", + "rule_id": "817b3030097446a38b3b842bf528e112" + }), + JSON.stringify({ + "scope": "test", + "name": "file3", + "rse_id": "c8b8113ddcdb4ec78e0846171e594280", + "rse": "XRD3", + "state": "REPLICATING", + "rule_id": "817b3030097446a38b3b842bf528e112" + }), + JSON.stringify({ + "scope": "test", + "name": "file4", + "rse_id": "c8b8113ddcdb4ec78e0846171e594280", + "rse": "XRD3", + "state": "REPLICATING", + "rule_id": "817b3030097446a38b3b842bf528e112" + }) + ] + + const listRuleReplicaLocksEndpoint: MockEndpoint = { + url: `${MockRucioServerFactory.RUCIO_HOST}/rules/817b3030097446a38b3b842bf528e112/locks`, + method: 'GET', + endsWith: '817b3030097446a38b3b842bf528e112/locks', + response: { + status: 200, + headers: { + 'Content-Type': 'application/x-json-stream', + }, + body: Readable.from(replicaLockStates.join('\n')) + } + + } + MockRucioServerFactory.createMockRucioServer(true, [getRuleEndpoint, listRuleReplicaLocksEndpoint]); + }); + afterEach(() => { + fetchMock.dontMock(); + }); + + it("Should fetch details of a rule", async () => { + const ruleGateway: RuleGatewayOutputPort = appContainer.get(GATEWAYS.RULE); + const listRuleLockStatesDTO: BaseStreamableDTO = await ruleGateway.listRuleReplicaLockStates(MockRucioServerFactory.VALID_RUCIO_TOKEN, '817b3030097446a38b3b842bf528e112'); + expect(listRuleLockStatesDTO.status).toEqual('success'); + + const ruleStream = listRuleLockStatesDTO.stream; + if(ruleStream == null || ruleStream == undefined) { + fail('Rule stream is null or undefined'); + } + + const recievedData = await collectStreamedData(ruleStream); + expect(recievedData.length).toEqual(4); + expect(recievedData[0].name).toEqual('file1'); + expect(recievedData[0].state).toEqual(LockState.REPLICATING); + + }); +}) \ No newline at end of file diff --git a/test/gateway/rule/rule-gateway-list-rules-for-account.test.ts b/test/gateway/rule/rule-gateway-list-rules-for-account.test.ts new file mode 100644 index 000000000..0bf82cbb7 --- /dev/null +++ b/test/gateway/rule/rule-gateway-list-rules-for-account.test.ts @@ -0,0 +1,56 @@ +import { RuleDTO } from "@/lib/core/dto/rule-dto"; +import RuleGatewayOutputPort from "@/lib/core/port/secondary/rule-gateway-output-port"; +import appContainer from "@/lib/infrastructure/ioc/container-config"; +import GATEWAYS from "@/lib/infrastructure/ioc/ioc-symbols-gateway"; +import { Readable } from "stream"; +import MockRucioServerFactory, { MockEndpoint } from "test/fixtures/rucio-server"; +import { collectStreamedData } from "test/fixtures/stream-test-utils"; + + +describe("RuleGateway", () => { + beforeEach(() => { + fetchMock.doMock(); + const ruleStream = Readable.from([ + JSON.stringify({"error": null, "locks_stuck_cnt": 0, "ignore_availability": false, "meta": null, "subscription_id": null, "rse_expression": "XRD1", "source_replica_expression": null, "ignore_account_limit": false, "created_at": "Mon, 27 Nov 2023 17:55:34 UTC", "account": "root", "copies": 1, "activity": "User Subscriptions", "priority": 3, "updated_at": "Mon, 27 Nov 2023 17:56:00 UTC", "scope": "test", "expires_at": null, "grouping": "DATASET", "name": "file1", "weight": null, "notification": "NO", "comments": null, "did_type": "FILE", "locked": false, "stuck_at": null, "child_rule_id": null, "state": "OK", "locks_ok_cnt": 1, "purge_replicas": false, "eol_at": null, "id": "e456aa5c7ae04c1cbd6c1bf9c9e6621d", "locks_replicating_cnt": 0, "split_container": false, "bytes": 10485760}), + JSON.stringify({"error": null, "locks_stuck_cnt": 0, "ignore_availability": false, "meta": null, "subscription_id": null, "rse_expression": "XRD1", "source_replica_expression": null, "ignore_account_limit": false, "created_at": "Mon, 27 Nov 2023 17:56:03 UTC", "account": "root", "copies": 1, "activity": "User Subscriptions", "priority": 3, "updated_at": "Mon, 27 Nov 2023 17:56:29 UTC", "scope": "test", "expires_at": null, "grouping": "DATASET", "name": "file2", "weight": null, "notification": "NO", "comments": null, "did_type": "FILE", "locked": false, "stuck_at": null, "child_rule_id": null, "state": "OK", "locks_ok_cnt": 1, "purge_replicas": false, "eol_at": null, "id": "f6339c0ab1814ee289ecfdb5e8011909", "locks_replicating_cnt": 0, "split_container": false, "bytes": 10485760}), + JSON.stringify({"error": null, "locks_stuck_cnt": 0, "ignore_availability": false, "meta": null, "subscription_id": null, "rse_expression": "XRD2", "source_replica_expression": null, "ignore_account_limit": false, "created_at": "Mon, 27 Nov 2023 17:56:32 UTC", "account": "root", "copies": 1, "activity": "User Subscriptions", "priority": 3, "updated_at": "Mon, 27 Nov 2023 17:56:58 UTC", "scope": "test", "expires_at": null, "grouping": "DATASET", "name": "file3", "weight": null, "notification": "NO", "comments": null, "did_type": "FILE", "locked": false, "stuck_at": null, "child_rule_id": null, "state": "OK", "locks_ok_cnt": 1, "purge_replicas": false, "eol_at": null, "id": "5242276fa0314af495d57c13a1a1a990", "locks_replicating_cnt": 0, "split_container": false, "bytes": 10485760}), + ].join('\n')); + + const listRulesEndpoint: MockEndpoint = { + url: `${MockRucioServerFactory.RUCIO_HOST}/rules`, + method: 'GET', + includes: 'rules', + response: { + status: 200, + headers: { + 'Content-Type': 'application/x-json-stream', + }, + body: ruleStream + } + } + + MockRucioServerFactory.createMockRucioServer(true, [listRulesEndpoint]); + }); + + afterEach(() => { + fetchMock.dontMock(); + }); + + it("Should fetch a list of rules", async () => { + const ruleGateway: RuleGatewayOutputPort = appContainer.get(GATEWAYS.RULE); + const listRulesDTO = await ruleGateway.listRules(MockRucioServerFactory.VALID_RUCIO_TOKEN); + expect(listRulesDTO.status).toEqual('success'); + + const ruleStream = listRulesDTO.stream; + if(ruleStream == null || ruleStream == undefined) { + fail('Rule stream is null or undefined'); + } + + const recievedData = await collectStreamedData(ruleStream); + expect(recievedData.length).toEqual(3); + expect(recievedData[0].id).toEqual('e456aa5c7ae04c1cbd6c1bf9c9e6621d'); + expect(recievedData[0].name).toEqual('file1'); + + }); + +}); \ No newline at end of file