diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index a8b946fd1..c375d625f 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -48,6 +48,7 @@ export class quay { // TODO: Make injected values into services and move to NgModule.providers, as constants are not supported in Angular 2 angular .module(quay.name) + .factory("FileReaderFactory", () => () => new FileReader()) .constant('NAME_PATTERNS', NAME_PATTERNS) .constant('INJECTED_CONFIG', INJECTED_CONFIG) .constant('INJECTED_FEATURES', INJECTED_FEATURES) diff --git a/static/js/services/dockerfile/dockerfile.service.impl.spec.ts b/static/js/services/dockerfile/dockerfile.service.impl.spec.ts index 041f31e64..0848a85d4 100644 --- a/static/js/services/dockerfile/dockerfile.service.impl.spec.ts +++ b/static/js/services/dockerfile/dockerfile.service.impl.spec.ts @@ -2,6 +2,116 @@ import { DockerfileServiceImpl, DockerfileInfoImpl } from './dockerfile.service. import Spy = jasmine.Spy; +describe("DockerfileServiceImpl", () => { + var dockerfileServiceImpl: DockerfileServiceImpl; + var dataFileServiceMock: any; + var configMock: any; + var fileReaderMock: FileReader; + + beforeEach(() => { + dataFileServiceMock = jasmine.createSpyObj('dataFileServiceMock', [ + 'readDataArrayAsPossibleArchive', + 'arrayToString', + 'blobToString', + ]); + configMock = jasmine.createSpyObj('configMock', ['getDomain']); + fileReaderMock = new FileReader(); + dockerfileServiceImpl = new DockerfileServiceImpl(dataFileServiceMock, configMock, () => fileReaderMock); + }); + + describe("getDockerfile", () => { + var file: any; + var readAsFileBufferSpy: Spy; + + beforeEach(() => { + dataFileServiceMock.readDataArrayAsPossibleArchive.and.callFake((buf, success, failure) => { + var files: any[] = []; + failure(files); + }); + + dataFileServiceMock.arrayToString.and.callFake((buf, callback) => { + var contents: string = ""; + callback(contents); + }); + + dataFileServiceMock.blobToString.and.callFake((blob, callback) => { + var contents: string = ""; + callback(contents); + }); + + spyOn(DockerfileInfoImpl, "forData").and.returnValue(null); + readAsFileBufferSpy = spyOn(fileReaderMock, "readAsArrayBuffer").and.callFake(() => { + var event: any = {target: {result: file}}; + fileReaderMock.onload(event); + }); + + file = "FROM quay.io/coreos/nginx:latest"; + }); + + it("calls datafile service to read given file as possible archive file", (done) => { + dockerfileServiceImpl.getDockerfile(file, + (dockerfile: DockerfileInfoImpl) => { + fail("Should not invoke success callback"); + done(); + }, + (error: Event | string) => { + expect(readAsFileBufferSpy.calls.argsFor(0)[0]).toEqual(file); + expect(dataFileServiceMock.readDataArrayAsPossibleArchive).toHaveBeenCalled(); + done(); + }); + }); + + it("calls datafile service to convert file to string if given file is not an archive", (done) => { + dockerfileServiceImpl.getDockerfile(file, + (dockerfile: DockerfileInfoImpl) => { + fail("Should not invoke success callback"); + done(); + }, + (error: Event | string) => { + expect(dataFileServiceMock.arrayToString.calls.argsFor(0)[0]).toEqual(file); + done(); + }); + }); + + it("calls failure callback if given non-archive file that is not a valid Dockerfile", (done) => { + dockerfileServiceImpl.getDockerfile(file, + (dockerfile: DockerfileInfoImpl) => { + fail("Should not invoke success callback"); + done(); + }, + (error: Event | string) => { + expect(error).toEqual('File chosen is not a valid Dockerfile'); + done(); + }); + }); + + it("calls success callback with new DockerfileInfoImpl instance if given valid Dockerfile", (done) => { + done(); + }); + + it("calls failure callback if given archive file with no Dockerfile present in root directory", (done) => { + done(); + }); + + it("calls datafile service to convert blob to string if given file is an archive", (done) => { + done(); + }); + + it("calls failure callback if given archive file with invalid Dockerfile", (done) => { + done(); + }); + + it("calls success callback with new DockerfileInfoImpl instance if given archive with valid Dockerfile", (done) => { + done(); + }); + }); + + describe("extractDockerfile", () => { + // TODO + }); +}); + + describe("DockerfileInfoImpl", () => { var dockerfileInfoImpl: DockerfileInfoImpl; var contents: string; @@ -27,18 +137,75 @@ describe("DockerfileInfoImpl", () => { }); describe("getRegistryBaseImage", () => { + var domain: string; + beforeEach(() => { + domain = "quay.io"; + }); + it("returns null if instance's contents do not contain a 'FROM' command", () => { + var getBaseImageSpy: Spy = spyOn(dockerfileInfoImpl, "getBaseImage").and.returnValue(null); + + expect(dockerfileInfoImpl.getRegistryBaseImage()).toBe(null); + expect(getBaseImageSpy).toHaveBeenCalled(); + }); + + it("returns null if the domain of the instance's config does not match that of the base image", () => { + configMock.getDomain.and.returnValue(domain); + spyOn(dockerfileInfoImpl, "getBaseImage").and.returnValue('host.com'); + + expect(dockerfileInfoImpl.getRegistryBaseImage()).toBe(null); + expect(configMock.getDomain).toHaveBeenCalled(); + }); + + it("returns the registry base image", () => { + spyOn(dockerfileInfoImpl, "getBaseImage").and.returnValue(null); + }); }); describe("getBaseImage", () => { - var baseImageAndTag + var host: string; + var port: number; + var tag: string; + var image: string; - it("returns null if instance's contents do not contain a 'FROM' command", () => { - var getBaseImageAndTagSpy: Spy = spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue() + beforeEach(() => { + host = 'quay.io'; + port = 80; + tag = 'latest'; + image = 'coreos/nginx'; }); - it("returns") + it("returns null if instance's contents do not contain a 'FROM' command", () => { + var getBaseImageAndTagSpy: Spy = spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(null); + + expect(dockerfileInfoImpl.getBaseImage()).toBe(null); + expect(getBaseImageAndTagSpy).toHaveBeenCalled(); + }); + + it("returns the image name if in the format 'someimage'", () => { + spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(image); + + expect(dockerfileInfoImpl.getBaseImage()).toEqual(image); + }); + + it("returns the image name if in the format 'someimage:tag'", () => { + spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(`${image}:${tag}`); + + expect(dockerfileInfoImpl.getBaseImage()).toEqual(image); + }); + + it("returns the host, port, and image name if in the format 'host:port/someimage'", () => { + spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(`${host}:${port}/${image}`); + + expect(dockerfileInfoImpl.getBaseImage()).toEqual(`${host}:${port}/${image}`); + }); + + it("returns the host, port, and image name if in the format 'host:port/someimage:tag'", () => { + spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(`${host}:${port}/${image}:${tag}`); + + expect(dockerfileInfoImpl.getBaseImage()).toEqual(`${host}:${port}/${image}`); + }); }); describe("getBaseImageAndTag", () => { @@ -64,28 +231,3 @@ describe("DockerfileInfoImpl", () => { }); }); }); - - -describe("DockerfileServiceImpl", () => { - var dockerfileServiceImpl: DockerfileServiceImpl; - var dataFileServiceMock: any; - var configMock: any; - - beforeEach(() => { - dataFileServiceMock = jasmine.createSpyObj('dataFileServiceMock', [ - 'readDataArrayAsPossibleArchive', - 'arrayToString', - 'blobToString', - ]); - configMock = jasmine.createSpyObj('configMock', ['getDomain']); - dockerfileServiceImpl = new DockerfileServiceImpl(dataFileServiceMock, configMock); - }); - - describe("getDockerfile", () => { - - }); - - describe("extractDockerfile", () => { - - }); -}); \ No newline at end of file diff --git a/static/js/services/dockerfile/dockerfile.service.impl.ts b/static/js/services/dockerfile/dockerfile.service.impl.ts index fbeb02c71..6783e8786 100644 --- a/static/js/services/dockerfile/dockerfile.service.impl.ts +++ b/static/js/services/dockerfile/dockerfile.service.impl.ts @@ -5,21 +5,32 @@ import { Injectable } from 'angular-ts-decorators'; @Injectable(DockerfileService.name) export class DockerfileServiceImpl implements DockerfileService { - constructor(private DataFileService: any, private Config: any) { - console.log(`=================== DockerfileServiceImpl ==========================`); + constructor(private DataFileService: any, private Config: any, private FileReaderFactory: () => FileReader) { + + } + + public extractDockerfile(file: any): Promise { + return new Promise((resolve, reject) => { + // TODO: Replace callbacks with promises + }); } public getDockerfile(file: any, success: (dockerfile: DockerfileInfoImpl) => void, failure: (error: Event | string) => void): void { - var reader: FileReader = new FileReader(); - reader.onload = (event: Event) => { - var dataArray: any = reader.result; - this.DataFileService.readDataArrayAsPossibleArchive(dataArray, (files) => { - this.processFiles(files, dataArray, success, failure); - }, () => { + var reader: FileReader = this.FileReaderFactory(); + reader.onload = (event: any) => { + + // FIXME: Debugging + console.log(event.target.result); + + this.DataFileService.readDataArrayAsPossibleArchive(event.target.result, + (files: any[]) => { + this.processFiles(files, event.target.result, success, failure); + }, + () => { // Not an archive. Read directly as a single file. - this.processFiles([], dataArray, success, failure); + this.processFiles([], event.target.result, success, failure); }); }; @@ -27,13 +38,7 @@ export class DockerfileServiceImpl implements DockerfileService { reader.readAsArrayBuffer(file); } - public extractDockerfile(file: any): Promise { - return new Promise((resolve, reject) => { - // TODO: Replace callbacks with promise - }); - } - - private processFiles(files: any, + private processFiles(files: any[], dataArray: any[], success: (dockerfile: DockerfileInfoImpl) => void, failure: (error: ErrorEvent | string) => void): void { @@ -41,8 +46,8 @@ export class DockerfileServiceImpl implements DockerfileService { // treat it as a single Dockerfile. if (files.length == 0) { this.DataFileService.arrayToString(dataArray, (contents: string) => { - var result = DockerfileInfoImpl.forData(contents, Object.assign({}, this.Config)); - if (!result) { + var result: DockerfileInfoImpl | null = DockerfileInfoImpl.forData(contents, this.Config); + if (result == null) { failure('File chosen is not a valid Dockerfile'); return; } @@ -56,8 +61,8 @@ export class DockerfileServiceImpl implements DockerfileService { files.forEach((file) => { if (file['name'] == 'Dockerfile') { this.DataFileService.blobToString(file.toBlob(), (contents: string) => { - var result: DockerfileInfoImpl | null = DockerfileInfoImpl.forData(contents, Object.assign({}, this.Config)); - if (result != null) { + var result: DockerfileInfoImpl | null = DockerfileInfoImpl.forData(contents, this.Config); + if (result == null) { failure('Dockerfile inside archive is not a valid Dockerfile'); return; } @@ -104,7 +109,7 @@ export class DockerfileInfoImpl implements DockerfileInfo { } public getBaseImage(): string | null { - var imageAndTag = this.getBaseImageAndTag(); + const imageAndTag = this.getBaseImageAndTag(); if (!imageAndTag) { return null; } @@ -114,15 +119,15 @@ export class DockerfileInfoImpl implements DockerfileInfo { // 2) someimage:tag // 3) host:port/someimage // 4) host:port/someimage:tag - var lastIndex = imageAndTag.lastIndexOf(':'); - if (lastIndex < 0) { + const lastIndex: number = imageAndTag.lastIndexOf(':'); + if (lastIndex == -1) { return imageAndTag; } // Otherwise, check if there is a / in the portion after the split point. If so, // then the latter is part of the path (and not a tag). - var afterColon = imageAndTag.substring(lastIndex + 1); - if (afterColon.indexOf('/') >= 0) { + const afterColon: string = imageAndTag.substring(lastIndex + 1); + if (afterColon.indexOf('/') != -1) { return imageAndTag; }