Skip to content

Commit

Permalink
feat: Add support for upload and delete Avatar API
Browse files Browse the repository at this point in the history
  • Loading branch information
arjankowski committed Jul 12, 2022
1 parent bc6ea18 commit 7b31944
Show file tree
Hide file tree
Showing 13 changed files with 594 additions and 34 deletions.
92 changes: 61 additions & 31 deletions BoxSDK.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions IntegrationTests/BaseIntegrationSpecs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -235,4 +235,37 @@ class BaseIntegrationSpecs: QuickSpec {
}
}
}

// MARK: Users helper methods

func createUser(name: String, callback: @escaping (User) -> Void) {
waitUntil(timeout: .seconds(Constants.Timeout.default)) { done in
self.client.users.createAppUser(name: name) { result in
switch result {
case let .success(user):
callback(user)
case let .failure(error):
fail("Expected create call to suceeded, but instead got \(error)")
}

done()
}
}
}

func deleteUser(_ user: User?) {
guard let user = user else {
return
}

waitUntil(timeout: .seconds(Constants.Timeout.large)) { done in
self.client.users.delete(userId: user.id, force: true) { result in
if case let .failure(error) = result {
fail("Expected delete call to succeed, but instead got \(error)")
}

done()
}
}
}
}
2 changes: 1 addition & 1 deletion IntegrationTests/BoxClientIntegrationSpecs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class BoxClientIntegrationSpecs: BaseIntegrationSpecs {
}

afterSuite {
self.deleteFolder(self.rootFolder)
self.deleteFolder(self.rootFolder, recursive: true)
}

describe("BoxClient") {
Expand Down
2 changes: 1 addition & 1 deletion IntegrationTests/FileModuleIntegrationSpecs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class FileModuleIntegrationSpecs: BaseIntegrationSpecs {
}

afterSuite {
self.deleteFolder(self.rootFolder)
self.deleteFolder(self.rootFolder, recursive: true)
}

describe("File Module") {
Expand Down
2 changes: 1 addition & 1 deletion IntegrationTests/FolderModuleIntegrationSpecs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class FolderModuleIntegrationSpecs: BaseIntegrationSpecs {
}

afterSuite {
self.deleteFolder(self.rootFolder)
self.deleteFolder(self.rootFolder, recursive: true)
}

describe("Folders Module") {
Expand Down
145 changes: 145 additions & 0 deletions IntegrationTests/UsersModuleIntegrationSpecs.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
//
// UsersModuleIntegrationSpecs.swift
// BoxSDKIntegrationTests-iOS
//
// Created by Artur Jankowski on 11/07/2022.
// Copyright © 2022 box. All rights reserved.
//

@testable import BoxSDK
import Nimble
import Quick

class UsersModuleIntegrationSpecs: BaseIntegrationSpecs {
override func spec() {
beforeSuite {
self.initializeClient()
}

describe("Users Module") {
context("avatar") {
var user: User?

beforeEach {
self.createUser(name: NameGenerator.getUniqueName(for: "user")) { createdUser in
user = createdUser
}
}

afterEach {
self.deleteUser(user)
user = nil
}

it("should correctly add get delete using Data") {
guard let user = user else {
fail("An error occurred during setup initial data")
return
}

// Add
let name = IntegrationTestResources.smallImage.fileName
let data = FileUtil.getFileContent(fileName: name)!

waitUntil(timeout: .seconds(Constants.Timeout.large)) { done in
self.client.users.uploadAvatar(userId: user.id, data: data, name: name) { result in
switch result {
case let .success(uploadItem):
expect(uploadItem.picUrls.preview).toNot(beNil() && beEmpty())
expect(uploadItem.picUrls.small).toNot(beNil() && beEmpty())
expect(uploadItem.picUrls.large).toNot(beNil() && beEmpty())
case let .failure(error):
fail("Expected uploadAvatar call to succeed, but instead got \(error)")
}

done()
}
}

// Get
waitUntil(timeout: .seconds(Constants.Timeout.large)) { done in
self.client.users.getAvatar(userId: user.id) { result in
switch result {
case let .success(data):
expect(data).toNot(beNil())
case let .failure(error):
fail("Expected getAvatar call to succeed, but instead got \(error)")
}

done()
}
}

// Delete
waitUntil(timeout: .seconds(Constants.Timeout.large)) { done in
self.client.users.deleteAvatar(userId: user.id) { result in
switch result {
case .success:
break
case let .failure(error):
fail("Expected deleteAvatar call to succeed, but instead got \(error)")
}

done()
}
}
}

it("should correctly add get delete using InputStream") {
guard let user = user else {
fail("An error occurred during setup initial data")
return
}

// Add
let name = IntegrationTestResources.smallImage.fileName
let path = FileUtil.getFilePath(fileName: name)
let inputStream = InputStream(fileAtPath: path)!

waitUntil(timeout: .seconds(Constants.Timeout.large)) { done in
self.client.users.streamUploadAvatar(userId: user.id, stream: inputStream, name: name) { result in
switch result {
case let .success(uploadItem):
expect(uploadItem.picUrls.preview).toNot(beNil() && beEmpty())
expect(uploadItem.picUrls.small).toNot(beNil() && beEmpty())
expect(uploadItem.picUrls.large).toNot(beNil() && beEmpty())
case let .failure(error):
fail("Expected streamUploadAvatar call to succeed, but instead got \(error)")
}

done()
}
}

// Get
waitUntil(timeout: .seconds(Constants.Timeout.large)) { done in
self.client.users.getAvatar(userId: user.id) { result in
switch result {
case let .success(data):
expect(data).toNot(beNil())
case let .failure(error):
fail("Expected getAvatar call to succeed, but instead got \(error)")
}

done()
}
}

// Delete
waitUntil(timeout: .seconds(Constants.Timeout.large)) { done in
self.client.users.deleteAvatar(userId: user.id) { result in
switch result {
case .success:
break
case let .failure(error):
fail("Expected deleteAvatar call to succeed, but instead got \(error)")
}

done()
}
}
}
}
}
}
}
48 changes: 48 additions & 0 deletions Sources/Core/MimeTypeProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// MimeTypeProvider.swift
// BoxSDK-iOS
//
// Created by Artur Jankowski on 08/07/2022.
// Copyright © 2022 box. All rights reserved.
//

import Foundation
#if os(iOS)
#if canImport(UniformTypeIdentifiers)
import UniformTypeIdentifiers
#endif

#if canImport(MobileCoreServices)
import MobileCoreServices
#endif
#endif

/// Provides method for converting given filename to mime type
enum MimeTypeProvider {

/// Converting given filename to mime type
///
/// - Parameters:
/// - filename: A file name with extension (e.g. image.png)
/// - Returns: A mime type
static func getMimeTypeFrom(filename: String) -> String {
let defaultMimeType = "application/octet-stream"
let url = NSURL(fileURLWithPath: filename)

guard let pathExtension = url.pathExtension else {
return defaultMimeType
}

if #available(iOS 14, *) {
return UTType(filenameExtension: pathExtension)?.preferredMIMEType ?? defaultMimeType
}
else {
if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as NSString, nil)?.takeRetainedValue(),
let mimeType = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() {
return mimeType as String
}
}

return defaultMimeType
}
}
86 changes: 86 additions & 0 deletions Sources/Modules/UsersModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,77 @@ public class UsersModule {
)
}

/// Upload avatar image to user account. Supported formats are JPG, JPEG and PNG.
/// Maximum allowed file size is 1MB and resolution 1024x1024 pixels.
///
/// - Parameters:
/// - userId: The ID of the user.
/// - data: The content of image file as binary data.
/// - name: File name of the avatar image. File name should also contains file extension (.jpg, .jpeg or .png).
/// - progress: Closure where upload progress will be reported
/// - completion: Returns an `UserAvatarUpload` object which contains avatar urls if successful otherwise a BoxSDKError.
/// - Returns: BoxUploadTask
@discardableResult
public func uploadAvatar(
userId: String,
data: Data,
name: String,
progress: @escaping (Progress) -> Void = { _ in },
completion: @escaping Callback<UserAvatarUpload>
) -> BoxUploadTask {
print(name)
var body = MultipartForm()
body.appendFilePart(
name: "pic",
contents: InputStream(data: data),
length: data.count,
fileName: name,
mimeType: MimeTypeProvider.getMimeTypeFrom(filename: name)
)

return boxClient.post(
url: URL.boxAPIEndpoint("/2.0/users/\(userId)/avatar", configuration: boxClient.configuration),
multipartBody: body,
progress: progress,
completion: ResponseHandler.default(wrapping: completion)
)
}

/// Upload avatar image to user account. Supported formats are JPG, JPEG and PNG.
/// Maximum allowed file size is 1MB and resolution 1024x1024 pixels.
///
/// - Parameters:
/// - userId: The ID of the user.
/// - stream: An InputStream of an image for been uploaded.
/// - name: File name of the avatar image. File name should also contains file extension (.jpg, .jpeg or .png).
/// - progress: Closure where upload progress will be reported
/// - completion: Returns an `UserAvatarUpload` object which contains avatar urls if successful otherwise a BoxSDKError.
/// - Returns: BoxUploadTask
@discardableResult
public func streamUploadAvatar(
userId: String,
stream: InputStream,
name: String,
progress: @escaping (Progress) -> Void = { _ in },
completion: @escaping Callback<UserAvatarUpload>
) -> BoxUploadTask {
var body = MultipartForm()
body.appendFilePart(
name: "pic",
contents: stream,
length: 0,
fileName: name,
mimeType: MimeTypeProvider.getMimeTypeFrom(filename: name)
)

return boxClient.post(
url: URL.boxAPIEndpoint("/2.0/users/\(userId)/avatar", configuration: boxClient.configuration),
multipartBody: body,
progress: progress,
completion: ResponseHandler.default(wrapping: completion)
)
}

/// Get image of a user's avatar
///
/// - Parameters:
Expand All @@ -168,6 +239,21 @@ public class UsersModule {
)
}

/// Deletes a user's avatar image.
///
/// - Parameters:
/// - userId: The ID of the user.
/// - completion: Empty response in case of success or an error.
public func deleteAvatar(
userId: String,
completion: @escaping Callback<Void>
) {
boxClient.delete(
url: URL.boxAPIEndpoint("/2.0/users/\(userId)/avatar", configuration: boxClient.configuration),
completion: ResponseHandler.default(wrapping: completion)
)
}

/// Create a new managed user in an enterprise. This method only works for Box admins.
///
/// - Parameters:
Expand Down

0 comments on commit 7b31944

Please sign in to comment.