generated from VLADIMIR/template
add load applications
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"evening_detective_server/internal/modules/file_storage"
|
||||
"evening_detective_server/internal/modules/processor_jwt"
|
||||
"evening_detective_server/internal/modules/string_tools"
|
||||
"evening_detective_server/internal/services/file_service"
|
||||
proto "evening_detective_server/proto"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// fakeStorage — in-memory реализация IFileStorage для app-слоя.
|
||||
type fakeStorage struct {
|
||||
files map[string]*file_storage.File
|
||||
putErr error
|
||||
getErr error
|
||||
}
|
||||
|
||||
func newFakeStorage() *fakeStorage {
|
||||
return &fakeStorage{files: map[string]*file_storage.File{}}
|
||||
}
|
||||
|
||||
func (s *fakeStorage) Put(_ context.Context, f *file_storage.File) error {
|
||||
if s.putErr != nil {
|
||||
return s.putErr
|
||||
}
|
||||
cp := *f
|
||||
cp.Data = append([]byte(nil), f.Data...)
|
||||
s.files[f.Name] = &cp
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *fakeStorage) Get(_ context.Context, name string) (*file_storage.File, error) {
|
||||
if s.getErr != nil {
|
||||
return nil, s.getErr
|
||||
}
|
||||
f, ok := s.files[name]
|
||||
if !ok {
|
||||
return nil, file_storage.ErrFileNotFound
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (s *fakeStorage) Delete(_ context.Context, name string) error {
|
||||
delete(s.files, name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *fakeStorage) MimeType(filename string) string {
|
||||
return file_storage.FileType(filename)
|
||||
}
|
||||
|
||||
func newTestFileServer(storage *fakeStorage, maxSize int) *server {
|
||||
if storage == nil {
|
||||
storage = newFakeStorage()
|
||||
}
|
||||
return &server{
|
||||
fileService: file_service.NewFileService(storage, string_tools.NewStringTools(), maxSize),
|
||||
}
|
||||
}
|
||||
|
||||
func ctxWithClaims(roles ...string) context.Context {
|
||||
return context.WithValue(context.Background(), "claims", &processor_jwt.JWTClaims{UserID: 1, Roles: roles})
|
||||
}
|
||||
|
||||
func pdfData(n int) []byte {
|
||||
b := make([]byte, n)
|
||||
copy(b, "%PDF-1.7 ")
|
||||
for i := 8; i < n; i++ {
|
||||
b[i] = 'x'
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func TestUploadFilePermissionDenied(t *testing.T) {
|
||||
s := newTestFileServer(nil, 1<<20)
|
||||
_, err := s.UploadFile(ctxWithClaims(), &proto.UploadFileReq{Filename: "clue.pdf", Data: pdfData(16)})
|
||||
if status.Code(err) != codes.PermissionDenied {
|
||||
t.Errorf("code = %v, want PermissionDenied", status.Code(err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadFileValidationInvalidArgument(t *testing.T) {
|
||||
s := newTestFileServer(nil, 1<<20)
|
||||
// Имя без расширения.
|
||||
_, err := s.UploadFile(ctxWithClaims("author"), &proto.UploadFileReq{Filename: "clue", Data: pdfData(16)})
|
||||
if status.Code(err) != codes.InvalidArgument {
|
||||
t.Errorf("code = %v, want InvalidArgument", status.Code(err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadFileTooLargeResourceExhausted(t *testing.T) {
|
||||
s := newTestFileServer(nil, 1024)
|
||||
_, err := s.UploadFile(ctxWithClaims("author"), &proto.UploadFileReq{Filename: "clue.pdf", Data: pdfData(1025)})
|
||||
if status.Code(err) != codes.ResourceExhausted {
|
||||
t.Errorf("code = %v, want ResourceExhausted", status.Code(err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadFilePutErrorInternal(t *testing.T) {
|
||||
storage := newFakeStorage()
|
||||
storage.putErr = errors.New("s3 недоступен")
|
||||
s := newTestFileServer(storage, 1<<20)
|
||||
|
||||
_, err := s.UploadFile(ctxWithClaims("author"), &proto.UploadFileReq{Filename: "clue.pdf", Data: pdfData(16)})
|
||||
if status.Code(err) != codes.Internal {
|
||||
t.Errorf("code = %v, want Internal", status.Code(err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadFileSuccess(t *testing.T) {
|
||||
s := newTestFileServer(nil, 1<<20)
|
||||
rsp, err := s.UploadFile(ctxWithClaims("author"), &proto.UploadFileReq{Filename: "clue.pdf", Data: pdfData(16)})
|
||||
if err != nil {
|
||||
t.Fatalf("UploadFile: %v", err)
|
||||
}
|
||||
if rsp.Filename == "" {
|
||||
t.Errorf("Filename пустой")
|
||||
}
|
||||
if rsp.FileType != "pdf" {
|
||||
t.Errorf("FileType = %q, want pdf", rsp.FileType)
|
||||
}
|
||||
if rsp.Error != "" {
|
||||
t.Errorf("Error = %q, want пусто", rsp.Error)
|
||||
}
|
||||
}
|
||||
|
||||
// mockServerTransportStream — заглушка для grpc.SetHeader в юнит-тестах.
|
||||
type mockServerTransportStream struct {
|
||||
grpc.ServerTransportStream
|
||||
header metadata.MD
|
||||
}
|
||||
|
||||
func (m *mockServerTransportStream) SetHeader(md metadata.MD) error {
|
||||
m.header = md
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestDownloadFileNotFound(t *testing.T) {
|
||||
s := newTestFileServer(nil, 1<<20)
|
||||
_, err := s.DownloadFile(context.Background(), &proto.DownloadFileReq{Filename: "missing.pdf"})
|
||||
if status.Code(err) != codes.NotFound {
|
||||
t.Errorf("code = %v, want NotFound", status.Code(err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadFileInternal(t *testing.T) {
|
||||
storage := newFakeStorage()
|
||||
storage.getErr = errors.New("s3 недоступен")
|
||||
s := newTestFileServer(storage, 1<<20)
|
||||
|
||||
_, err := s.DownloadFile(context.Background(), &proto.DownloadFileReq{Filename: "clue.pdf"})
|
||||
if status.Code(err) != codes.Internal {
|
||||
t.Errorf("code = %v, want Internal", status.Code(err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadFileSuccess(t *testing.T) {
|
||||
storage := newFakeStorage()
|
||||
storage.files["clue.pdf"] = &file_storage.File{
|
||||
Name: "clue.pdf",
|
||||
Data: []byte("%PDF-1.7 content"),
|
||||
Mime: "application/pdf",
|
||||
}
|
||||
s := newTestFileServer(storage, 1<<20)
|
||||
|
||||
stream := &mockServerTransportStream{}
|
||||
ctx := grpc.NewContextWithServerTransportStream(context.Background(), stream)
|
||||
|
||||
body, err := s.DownloadFile(ctx, &proto.DownloadFileReq{Filename: "clue.pdf"})
|
||||
if err != nil {
|
||||
t.Fatalf("DownloadFile: %v", err)
|
||||
}
|
||||
if string(body.Data) != "%PDF-1.7 content" {
|
||||
t.Errorf("Data = %q", body.Data)
|
||||
}
|
||||
if body.ContentType != "application/pdf" {
|
||||
t.Errorf("ContentType = %q, want application/pdf", body.ContentType)
|
||||
}
|
||||
if got := stream.header.Get("X-Content-Type-Options"); len(got) != 1 || got[0] != "nosniff" {
|
||||
t.Errorf("X-Content-Type-Options = %v, want nosniff", got)
|
||||
}
|
||||
}
|
||||
+30
-11
@@ -312,27 +312,46 @@ func (s *server) GetPermissions(ctx context.Context, req *proto.GetPermissionsRe
|
||||
}
|
||||
|
||||
func (s *server) UploadFile(ctx context.Context, req *proto.UploadFileReq) (*proto.UploadFileRsp, error) {
|
||||
filename, err := s.fileService.UploadFile(
|
||||
ctx,
|
||||
&file_storage.File{
|
||||
Name: req.Filename,
|
||||
Data: req.Data,
|
||||
},
|
||||
)
|
||||
// Файлы (в т.ч. улики) загружают авторы сценариев в редакторе.
|
||||
claims := ctx.Value("claims").(*processor_jwt.JWTClaims)
|
||||
if !roles.HasRole(claims, roles.Author) {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
||||
}
|
||||
|
||||
filename, fileType, err := s.fileService.UploadFile(ctx, req.Filename, req.Data)
|
||||
if err != nil {
|
||||
return &proto.UploadFileRsp{
|
||||
Error: err.Error(),
|
||||
}, nil
|
||||
switch {
|
||||
case errors.Is(err, file_service.ErrFileTooLarge):
|
||||
return nil, status.Errorf(codes.ResourceExhausted, "%v", err)
|
||||
case errors.Is(err, file_service.ErrEmptyName),
|
||||
errors.Is(err, file_service.ErrEmptyData),
|
||||
errors.Is(err, file_service.ErrInvalidExtension),
|
||||
errors.Is(err, file_service.ErrInvalidContentType):
|
||||
return nil, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
default:
|
||||
return nil, status.Errorf(codes.Internal, "%v", err)
|
||||
}
|
||||
}
|
||||
return &proto.UploadFileRsp{
|
||||
Filename: filename,
|
||||
FileType: fileType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *server) DownloadFile(ctx context.Context, req *proto.DownloadFileReq) (*httpbody.HttpBody, error) {
|
||||
file, err := s.fileService.DownloadFile(ctx, req.Filename)
|
||||
if err != nil {
|
||||
return &httpbody.HttpBody{}, nil
|
||||
switch {
|
||||
case errors.Is(err, file_storage.ErrFileNotFound):
|
||||
return nil, status.Errorf(codes.NotFound, "%v", err)
|
||||
default:
|
||||
return nil, status.Errorf(codes.Internal, "%v", err)
|
||||
}
|
||||
}
|
||||
// nosniff — при публичном скачивании браузер не должен угадывать
|
||||
// Content-Type по содержимому (защита от stored-XSS при расхождении).
|
||||
if err := grpc.SetHeader(ctx, metadata.Pairs("X-Content-Type-Options", "nosniff")); err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to set response header: %v", err)
|
||||
}
|
||||
return &httpbody.HttpBody{
|
||||
Data: file.Data,
|
||||
|
||||
@@ -109,8 +109,9 @@ func mapKeys(o []*storytelling.Key) []*proto.Key {
|
||||
|
||||
func mapApplication(o *storytelling.Application) *proto.Application {
|
||||
return &proto.Application{
|
||||
Name: o.Name,
|
||||
Image: o.Image,
|
||||
Name: o.Name,
|
||||
Image: o.Image,
|
||||
FileType: o.FileType,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,8 +168,9 @@ func convertKeys(o []*proto.Key) []*storytelling.Key {
|
||||
|
||||
func convertApplication(o *proto.Application) *storytelling.Application {
|
||||
return &storytelling.Application{
|
||||
Name: textFormatter.FormatString(o.Name),
|
||||
Image: o.Image,
|
||||
Name: textFormatter.FormatString(o.Name),
|
||||
Image: o.Image,
|
||||
FileType: o.FileType,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user