Skip to content

Commit

Permalink
EW-539 adding Learnstore links to the Common Cartridge export (#4599)
Browse files Browse the repository at this point in the history
  • Loading branch information
psachmann authored Mar 15, 2024
1 parent d78ea8e commit 2bee608
Show file tree
Hide file tree
Showing 117 changed files with 4,489 additions and 1,588 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export class KeycloakAdministrationService {

private static AUTHORIZATION_TIMEBOX_MS = 59 * 1000;

public constructor(
constructor(
private readonly kcAdminClient: KeycloakAdminClient,
@Inject(KeycloakSettings) private readonly kcSettings: IKeycloakSettings
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { faker } from '@faker-js/faker';
import AdmZip from 'adm-zip';
import {
CommonCartridgeElementType,
CommonCartridgeIntendedUseType,
CommonCartridgeResourceType,
CommonCartridgeVersion,
} from '../common-cartridge.enums';
import { CommonCartridgeElementProps } from '../elements/common-cartridge-element-factory';
import { CommonCartridgeResourceProps } from '../resources/common-cartridge-resource-factory';
import { CommonCartridgeFileBuilder } from './common-cartridge-file-builder';
import { CommonCartridgeOrganizationBuilderOptions } from './common-cartridge-organization-builder';

describe('CommonCartridgeFileBuilder', () => {
const getFileContentAsString = (zip: AdmZip, path: string): string | undefined =>
zip.getEntry(path)?.getData().toString();

describe('build', () => {
describe('when a common cartridge archive has been created', () => {
const setup = async () => {
const metadataProps: CommonCartridgeElementProps = {
type: CommonCartridgeElementType.METADATA,
title: faker.lorem.words(),
creationDate: new Date(),
copyrightOwners: ['John Doe', 'Jane Doe'],
};
const organizationOptions: CommonCartridgeOrganizationBuilderOptions = {
identifier: faker.string.uuid(),
title: faker.lorem.words(),
};
const resourceProps: CommonCartridgeResourceProps = {
type: CommonCartridgeResourceType.WEB_CONTENT,
identifier: faker.string.uuid(),
title: faker.lorem.words(),
html: faker.lorem.paragraphs(),
intendedUse: CommonCartridgeIntendedUseType.UNSPECIFIED,
};
const builder = new CommonCartridgeFileBuilder({
version: CommonCartridgeVersion.V_1_1_0,
identifier: faker.string.uuid(),
});

builder
.addMetadata(metadataProps)
.addOrganization(organizationOptions)
.addResource(resourceProps)
.addSubOrganization(organizationOptions)
.addResource(resourceProps)
.addSubOrganization(organizationOptions)
.addResource(resourceProps);

const archive = new AdmZip(await builder.build());

return { archive, metadataProps, organizationOptions, resourceProps };
};

it('should have a imsmanifest.xml in archive root', async () => {
const { archive } = await setup();

const manifest = getFileContentAsString(archive, 'imsmanifest.xml');

expect(manifest).toBeDefined();
});

it('should have included the resource in organization folder', async () => {
const { archive, organizationOptions, resourceProps } = await setup();

const resource = getFileContentAsString(
archive,
`${organizationOptions.identifier}/${resourceProps.identifier}.html`
);

expect(resource).toBeDefined();
});

it('should have included the resource in sub-organization folder', async () => {
const { archive, organizationOptions, resourceProps } = await setup();

const resource = getFileContentAsString(
archive,
`${organizationOptions.identifier}/${organizationOptions.identifier}/${resourceProps.identifier}.html`
);

expect(resource).toBeDefined();
});

it('should have included the resource in sub-sub-organization folder', async () => {
const { archive, organizationOptions, resourceProps } = await setup();

const resource = getFileContentAsString(
archive,
`${organizationOptions.identifier}/${organizationOptions.identifier}/${organizationOptions.identifier}/${resourceProps.identifier}.html`
);

expect(resource).toBeDefined();
});
});

describe('when metadata has not been provide', () => {
const sut = new CommonCartridgeFileBuilder({
version: CommonCartridgeVersion.V_1_1_0,
identifier: faker.string.uuid(),
});

it('should throw an error', async () => {
await expect(sut.build()).rejects.toThrow('Metadata is not defined');
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import AdmZip from 'adm-zip';
import { CommonCartridgeResourceType, CommonCartridgeVersion } from '../common-cartridge.enums';
import {
CommonCartridgeElementFactory,
CommonCartridgeElementProps,
} from '../elements/common-cartridge-element-factory';
import { CommonCartridgeElement, CommonCartridgeResource } from '../interfaces';
import { CommonCartridgeResourceFactory } from '../resources/common-cartridge-resource-factory';
import { OmitVersion } from '../utils';
import {
CommonCartridgeOrganizationBuilder,
CommonCartridgeOrganizationBuilderOptions,
} from './common-cartridge-organization-builder';

export type CommonCartridgeFileBuilderProps = {
version: CommonCartridgeVersion;
identifier: string;
};

export class CommonCartridgeFileBuilder {
private readonly archive: AdmZip = new AdmZip();

private readonly organizationBuilders = new Array<CommonCartridgeOrganizationBuilder>();

private readonly resources = new Array<CommonCartridgeResource>();

private metadata?: CommonCartridgeElement;

constructor(private readonly props: CommonCartridgeFileBuilderProps) {}

public addMetadata(props: CommonCartridgeElementProps): CommonCartridgeFileBuilder {
this.metadata = CommonCartridgeElementFactory.createElement({
version: this.props.version,
...props,
});

return this;
}

public addOrganization(
props: OmitVersion<CommonCartridgeOrganizationBuilderOptions>
): CommonCartridgeOrganizationBuilder {
const builder = new CommonCartridgeOrganizationBuilder(
{ ...props, version: this.props.version },
(resource: CommonCartridgeResource) => this.resources.push(resource)
);

this.organizationBuilders.push(builder);

return builder;
}

public async build(): Promise<Buffer> {
if (!this.metadata) {
throw new Error('Metadata is not defined');
}

const organizations = this.organizationBuilders.map((builder) => builder.build());
const manifest = CommonCartridgeResourceFactory.createResource({
type: CommonCartridgeResourceType.MANIFEST,
version: this.props.version,
identifier: this.props.identifier,
metadata: this.metadata,
organizations,
resources: this.resources,
});

for (const resources of this.resources) {
if (!resources.canInline()) {
this.archive.addFile(resources.getFilePath(), Buffer.from(resources.getFileContent()));
}
}

this.archive.addFile(manifest.getFilePath(), Buffer.from(manifest.getFileContent()));

const buffer = await this.archive.toBufferPromise();

return buffer;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { faker } from '@faker-js/faker/locale/af_ZA';
import { createCommonCartridgeWebContentResourcePropsV110 } from '../../testing/common-cartridge-resource-props.factory';
import { CommonCartridgeVersion } from '../common-cartridge.enums';
import { CommonCartridgeElement, CommonCartridgeResource } from '../interfaces';
import {
CommonCartridgeOrganizationBuilder,
CommonCartridgeOrganizationBuilderOptions,
} from './common-cartridge-organization-builder';

describe('CommonCartridgeOrganizationBuilder', () => {
describe('build', () => {
describe('when building a Common Cartridge organization with resources', () => {
const setup = () => {
const resources = new Array<CommonCartridgeResource>();

const organizationOptions: CommonCartridgeOrganizationBuilderOptions = {
identifier: faker.string.uuid(),
title: faker.lorem.words(),
};

const resourceProps = createCommonCartridgeWebContentResourcePropsV110();

const sut = new CommonCartridgeOrganizationBuilder(
{
...organizationOptions,
version: CommonCartridgeVersion.V_1_1_0,
},
(resource) => resources.push(resource)
)
.addResource(resourceProps)
.addSubOrganization(organizationOptions)
.addResource(resourceProps)
.addSubOrganization(organizationOptions)
.addResource(resourceProps);

return { sut, resources };
};

it('should return a common cartridge element', () => {
const { sut, resources } = setup();

const element = sut.build();

expect(element).toBeInstanceOf(CommonCartridgeElement);
expect(resources.length).toBe(3);
});
});

describe('when building a Common Cartridge organization with items', () => {
const setup = () => {
const resources = new Array<CommonCartridgeResource>();

const organizationOptions: CommonCartridgeOrganizationBuilderOptions = {
identifier: faker.string.uuid(),
title: faker.lorem.words(),
};

const resourceProps = createCommonCartridgeWebContentResourcePropsV110();

const sut = new CommonCartridgeOrganizationBuilder(
{
...organizationOptions,
version: CommonCartridgeVersion.V_1_1_0,
},
(resource) => resources.push(resource)
)
.addResource(resourceProps)
.addSubOrganization(organizationOptions)
.addResource(resourceProps)
.addSubOrganization(organizationOptions)
.addResource(resourceProps)
.addResource(resourceProps);

return { sut, resources };
};

it('should return a common cartridge element', () => {
const { sut, resources } = setup();

const element = sut.build();

expect(element).toBeInstanceOf(CommonCartridgeElement);
expect(resources.length).toBe(4);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { CommonCartridgeElementType, CommonCartridgeVersion } from '../common-cartridge.enums';
import { CommonCartridgeElementFactory } from '../elements/common-cartridge-element-factory';
import { CommonCartridgeElement } from '../interfaces/common-cartridge-element.interface';
import { CommonCartridgeResource } from '../interfaces/common-cartridge-resource.interface';
import {
CommonCartridgeResourceFactory,
CommonCartridgeResourceProps,
} from '../resources/common-cartridge-resource-factory';
import { OmitVersionAndFolder } from '../utils';

export type CommonCartridgeOrganizationBuilderOptions =
OmitVersionAndFolder<CommonCartridgeOrganizationBuilderOptionsInternal>;

type CommonCartridgeOrganizationBuilderOptionsInternal = {
version: CommonCartridgeVersion;
identifier: string;
title: string;
folder?: string;
};

export class CommonCartridgeOrganizationBuilder {
private readonly resources: CommonCartridgeResource[] = [];

private readonly subOrganizations: CommonCartridgeOrganizationBuilder[] = [];

constructor(
protected readonly options: CommonCartridgeOrganizationBuilderOptionsInternal,
private readonly addResourceToFileBuilder: (resource: CommonCartridgeResource) => void
) {}

private get folder(): string {
return this.options.folder ? `${this.options.folder}/${this.options.identifier}` : this.options.identifier;
}

public addSubOrganization(
options: OmitVersionAndFolder<CommonCartridgeOrganizationBuilderOptions>
): CommonCartridgeOrganizationBuilder {
const subOrganization = new CommonCartridgeOrganizationBuilder(
{ ...options, version: this.options.version, folder: this.folder },
(resource: CommonCartridgeResource) => this.addResourceToFileBuilder(resource)
);

this.subOrganizations.push(subOrganization);

return subOrganization;
}

public addResource(props: CommonCartridgeResourceProps): CommonCartridgeOrganizationBuilder {
const resource = CommonCartridgeResourceFactory.createResource({
version: this.options.version,
folder: this.folder,
...props,
});

this.resources.push(resource);
this.addResourceToFileBuilder(resource);

return this;
}

public build(): CommonCartridgeElement {
const organizationElement = CommonCartridgeElementFactory.createElement({
type: CommonCartridgeElementType.ORGANIZATION,
version: this.options.version,
identifier: this.options.identifier,
title: this.options.title,
items: this.buildItems(),
});

return organizationElement;
}

private buildItems(): (CommonCartridgeElement | CommonCartridgeResource)[] {
if (this.resources.length === 1 && this.subOrganizations.length === 0) {
return [...this.resources];
}

const items = [...this.resources, ...this.subOrganizations.map((subOrganization) => subOrganization.build())];

return items;
}
}
Loading

0 comments on commit 2bee608

Please sign in to comment.