add load applications

This commit is contained in:
2026-08-30 21:17:32 +07:00
parent 3b15fb3a2b
commit c0b5ceb65f
28 changed files with 1536 additions and 139 deletions
+191
View File
@@ -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
View File
@@ -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,
+6 -4
View File
@@ -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,
}
}