-
Notifications
You must be signed in to change notification settings - Fork 320
Add functions to get temporary URLs for files #1780
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -327,6 +327,51 @@ class UploadFileResult { | |||||
Map<String, dynamic> toJson() => _$UploadFileResultToJson(this); | ||||||
} | ||||||
|
||||||
/// Get a temporary, authless partial URL to a realm-uploaded file. | ||||||
/// | ||||||
/// The URL returned allows a file to be viewed without requiring authentication, | ||||||
/// but it doesn't include secrets like the API key. This URL remains valid for | ||||||
/// 60 seconds. | ||||||
/// | ||||||
/// This endpoint is documented in the OpenAPI description: | ||||||
/// https://github.com/zulip/zulip/blob/main/zerver/openapi/zulip.yaml | ||||||
/// under the name `get_file_temporary_url`. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I couldn't find a match of |
||||||
Future<Uri> getFileTemporaryUrl(ApiConnection connection, { | ||||||
required String filePath, | ||||||
}) async { | ||||||
final response = await connection.get('getFileTemporaryUrl', | ||||||
(json) => json['url'], | ||||||
filePath.substring(1), // remove leading slash to avoid duplicate | ||||||
{}, | ||||||
); | ||||||
|
||||||
return Uri.parse('${connection.realmUrl}$response'); | ||||||
} | ||||||
|
||||||
/// A wrapper for [getFileTemporaryUrl] that returns null on failure. | ||||||
/// | ||||||
/// Validates that the URL is a realm-uploaded file before proceeding. | ||||||
Future<Uri?> tryGetFileTemporaryUrl( | ||||||
ApiConnection connection, { | ||||||
required Uri url, | ||||||
required Uri realmUrl, | ||||||
}) async { | ||||||
if (url.origin != realmUrl.origin) { | ||||||
return null; | ||||||
} | ||||||
|
||||||
final filePath = url.path; | ||||||
if (!RegExp(r'^/user_uploads/[0-9]+/.+$').hasMatch(filePath)) { | ||||||
return null; | ||||||
} | ||||||
|
||||||
try { | ||||||
return await getFileTemporaryUrl(connection, filePath: filePath); | ||||||
} catch (e) { | ||||||
return null; | ||||||
} | ||||||
} | ||||||
|
||||||
/// https://zulip.com/api/add-reaction | ||||||
Future<void> addReaction(ApiConnection connection, { | ||||||
required int messageId, | ||||||
|
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -608,6 +608,79 @@ void main() { | |||||||||
}); | ||||||||||
}); | ||||||||||
|
||||||||||
group('getFileTemporaryUrl', () { | ||||||||||
test('constructs URL correctly from response', () { | ||||||||||
return FakeApiConnection.with_((connection) async { | ||||||||||
connection.prepare(json: { | ||||||||||
'url': '/user_uploads/temporary/abc123', | ||||||||||
'result': 'success', | ||||||||||
'msg': '', | ||||||||||
}); | ||||||||||
|
||||||||||
final result = await getFileTemporaryUrl(connection, | ||||||||||
filePath: '/user_uploads/1/2/testfile.jpg'); | ||||||||||
|
||||||||||
check(result.toString()).equals('${connection.realmUrl}/user_uploads/temporary/abc123'); | ||||||||||
check(connection.lastRequest).isA<http.Request>() | ||||||||||
..method.equals('GET') | ||||||||||
..url.path.equals('/api/v1/user_uploads/1/2/testfile.jpg'); | ||||||||||
}); | ||||||||||
}); | ||||||||||
|
||||||||||
test('returns temporary URL for valid realm file', () { | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test and the remaining test blocks in the group require an indentation. |
||||||||||
return FakeApiConnection.with_((connection) async { | ||||||||||
connection.prepare(json: { | ||||||||||
'url': '/user_uploads/temporary/abc123', | ||||||||||
'result': 'success', | ||||||||||
'msg': '', | ||||||||||
}); | ||||||||||
|
||||||||||
final result = await tryGetFileTemporaryUrl(connection, | ||||||||||
url: Uri.parse('${connection.realmUrl}user_uploads/123/testfile.jpg'), | ||||||||||
realmUrl: connection.realmUrl); | ||||||||||
|
||||||||||
check(result).isNotNull(); | ||||||||||
check(result.toString()).equals('${connection.realmUrl}/user_uploads/temporary/abc123'); | ||||||||||
}); | ||||||||||
}); | ||||||||||
|
||||||||||
test('returns null for non-realm URL', () { | ||||||||||
return FakeApiConnection.with_((connection) async { | ||||||||||
final result = await tryGetFileTemporaryUrl(connection, | ||||||||||
url: Uri.parse('https://example.com/image.jpg'), | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
The URL provided is both non-realm URL and a non-matching URL, but the test case is only about non-realm URL, so it's good to adjust it to only what the test case is about. That way, it's easy to validate it against what the test case claims. |
||||||||||
realmUrl: connection.realmUrl); | ||||||||||
|
||||||||||
check(result).isNull(); | ||||||||||
// Verify no API calls were made | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
or
Suggested change
Either complete a sentence that starts with a capital letter by adding a period at the end, or start it with a lowercase letter and omit the period. 🙂 |
||||||||||
check(connection.lastRequest).isNull(); | ||||||||||
}); | ||||||||||
}); | ||||||||||
|
||||||||||
test('returns null for non-matching URL pattern', () { | ||||||||||
return FakeApiConnection.with_((connection) async { | ||||||||||
final result = await tryGetFileTemporaryUrl(connection, | ||||||||||
url: Uri.parse('${connection.realmUrl}/invalid/path/file.jpg'), | ||||||||||
realmUrl: connection.realmUrl); | ||||||||||
|
||||||||||
check(result).isNull(); | ||||||||||
// Verify no API calls were made | ||||||||||
check(connection.lastRequest).isNull(); | ||||||||||
}); | ||||||||||
}); | ||||||||||
|
||||||||||
test('returns null when API request fails', () { | ||||||||||
return FakeApiConnection.with_((connection) async { | ||||||||||
connection.prepare( | ||||||||||
apiException: eg.apiBadRequest(message: 'Not found')); | ||||||||||
|
||||||||||
final result = await tryGetFileTemporaryUrl(connection, | ||||||||||
url: Uri.parse('${connection.realmUrl}/user_uploads/1/2/testfile.jpg'), | ||||||||||
realmUrl: connection.realmUrl); | ||||||||||
|
||||||||||
check(result).isNull(); | ||||||||||
}); | ||||||||||
}); | ||||||||||
}); | ||||||||||
group('addReaction', () { | ||||||||||
Future<void> checkAddReaction(FakeApiConnection connection, { | ||||||||||
required int messageId, | ||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: commit summary line
The two functions in the commit introduce new behavior (new API calls and logic), so it's not an NFC commit. Also, good to mention the routes introduced in the summary line.