Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ describe('FileListItemComponent', () => {
provide: ShareLinksService,
useValue: {
isUnlistedShare: async () => await Promise.resolve(false),
isUnlistedShareSync: () => false,
},
},
{ provide: EditService, useValue: mockEditService },
Expand Down Expand Up @@ -317,9 +318,7 @@ describe('FileListItemComponent', () => {
(router.routerState.snapshot as any).url = '/share/test';

const shareLinksService = TestBed.inject(ShareLinksService);
spyOn(shareLinksService, 'isUnlistedShare').and.returnValue(
Promise.resolve(false),
);
spyOn(shareLinksService, 'isUnlistedShareSync').and.returnValue(false);
component.item.isRecord = true;
component.item.type = 'type.record.image';

Expand All @@ -333,13 +332,30 @@ describe('FileListItemComponent', () => {
(router.routerState.snapshot as any).url = '/';
});

it('should always set real thumbnail URL on init', async () => {
it('should set real thumbnail URL on init outside share preview', async () => {
component.item.isRecord = true;
component.item.type = 'type.record.image';
component.item.thumbURL200 = 'https://example.com/thumb.jpg';

await component.ngOnInit();

expect(component.recordThumbnailUrl).toBe('https://example.com/thumb.jpg');
});

it('should set real thumbnail URL on init for unlisted share records', async () => {
const router = TestBed.inject(Router);
(router.routerState.snapshot as any).url = '/share/test';

const shareLinksService = TestBed.inject(ShareLinksService);
spyOn(shareLinksService, 'isUnlistedShareSync').and.returnValue(true);
component.item.isRecord = true;
component.item.type = 'type.record.image';
component.item.thumbURL200 = 'https://example.com/thumb.jpg';

await component.ngOnInit();

expect(component.recordThumbnailUrl).toBe('https://example.com/thumb.jpg');

(router.routerState.snapshot as any).url = '/';
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -250,12 +250,17 @@ export class FileListItemComponent
) {}

async ngOnInit() {
this.recordThumbnailUrl = GetThumbnail(this.item);
this.isUnlistedShare = this.shareLinksService.isUnlistedShareSync();
const isInSharePreview =
this.router.routerState.snapshot.url.includes('/share/');
if (isInSharePreview && !this.isUnlistedShare) {
this.recordThumbnailUrl = this.getRandomPreviewImage();
} else {
this.recordThumbnailUrl = GetThumbnail(this.item);
}
const date = new Date(this.item.displayDT);
this.date = getFormattedDate(date);

this.isUnlistedShare = await this.shareLinksService.isUnlistedShare();

this.dataService.registerItem(this.item);
if (this.item.type.includes('app')) {
this.allowActions = false;
Expand All @@ -266,10 +271,7 @@ export class FileListItemComponent
this.isPublicArchive = true;
}

if (this.router.routerState.snapshot.url.includes('/share/')) {
if (!this.isUnlistedShare) {
this.recordThumbnailUrl = this.getRandomPreviewImage();
}
if (isInSharePreview) {
this.allowActions = false;
this.isInSharePreview = true;
}
Expand Down
111 changes: 111 additions & 0 deletions src/app/share-links/services/share-links.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import { TestBed } from '@angular/core/testing';
import { NavigationEnd, Router } from '@angular/router';
import { Subject } from 'rxjs';
import { ShareLink } from '../models/share-link';
import { ShareLinksService } from './share-links.service';
import { ShareLinksApiService } from './share-links-api.service';

describe('ShareLinksService', () => {
let service: ShareLinksService;
let apiSpy: jasmine.SpyObj<ShareLinksApiService>;
let routerEvents: Subject<NavigationEnd>;

beforeEach(() => {
apiSpy = jasmine.createSpyObj('ShareLinksApiService', [
'getShareLinksByToken',
]);
routerEvents = new Subject<NavigationEnd>();

TestBed.configureTestingModule({
providers: [
ShareLinksService,
{ provide: ShareLinksApiService, useValue: apiSpy },
{ provide: Router, useValue: { events: routerEvents.asObservable() } },
],
});

Expand Down Expand Up @@ -84,4 +89,110 @@ describe('ShareLinksService', () => {
expect(secondCall).toBeTrue();
expect(apiSpy.getShareLinksByToken).toHaveBeenCalledTimes(1); // no second fetch
});

describe('primeForToken', () => {
it('should set the current token and populate shareLinks cache', async () => {
apiSpy.getShareLinksByToken.and.resolveTo([
{ accessRestrictions: 'none' } as ShareLink,
]);

await service.primeForToken('abc123');

expect(service.currentShareToken).toBe('abc123');
expect(apiSpy.getShareLinksByToken).toHaveBeenCalledWith(['abc123']);
});

it('should swallow API errors and leave the cache empty', async () => {
apiSpy.getShareLinksByToken.and.rejectWith(new Error('boom'));

await service.primeForToken('abc123');

expect(service.isUnlistedShareSync()).toBeFalse();
});

it('should skip the API call when the token is empty', async () => {
await service.primeForToken('');

expect(apiSpy.getShareLinksByToken).not.toHaveBeenCalled();
expect(service.isUnlistedShareSync()).toBeFalse();
});
});

describe('isUnlistedShareSync', () => {
it('should return false before priming', () => {
expect(service.isUnlistedShareSync()).toBeFalse();
});

it('should return true after priming an unlisted share', async () => {
apiSpy.getShareLinksByToken.and.resolveTo([
{ accessRestrictions: 'none' } as ShareLink,
]);
await service.primeForToken('abc123');

expect(service.isUnlistedShareSync()).toBeTrue();
});

it('should return false after priming a restricted share', async () => {
apiSpy.getShareLinksByToken.and.resolveTo([
{ accessRestrictions: 'approval' } as ShareLink,
]);
await service.primeForToken('abc123');

expect(service.isUnlistedShareSync()).toBeFalse();
});
});

describe('currentShareToken setter', () => {
it('should clear the shareLinks cache when the token changes', async () => {
apiSpy.getShareLinksByToken.and.resolveTo([
{ accessRestrictions: 'none' } as ShareLink,
]);
await service.primeForToken('abc123');

expect(service.isUnlistedShareSync()).toBeTrue();

service.currentShareToken = 'different';

expect(service.isUnlistedShareSync()).toBeFalse();
});

it('should preserve the shareLinks cache when the token does not change', async () => {
apiSpy.getShareLinksByToken.and.resolveTo([
{ accessRestrictions: 'none' } as ShareLink,
]);
await service.primeForToken('abc123');

service.currentShareToken = 'abc123';

expect(service.isUnlistedShareSync()).toBeTrue();
});
});

describe('router navigation cleanup', () => {
it('should clear cache when navigating off a share route', async () => {
apiSpy.getShareLinksByToken.and.resolveTo([
{ accessRestrictions: 'none' } as ShareLink,
]);
await service.primeForToken('abc123');

expect(service.isUnlistedShareSync()).toBeTrue();

routerEvents.next(new NavigationEnd(1, '/app/private', '/app/private'));

expect(service.currentShareToken).toBe('');
expect(service.isUnlistedShareSync()).toBeFalse();
});

it('should preserve cache when navigating within share routes', async () => {
apiSpy.getShareLinksByToken.and.resolveTo([
{ accessRestrictions: 'none' } as ShareLink,
]);
await service.primeForToken('abc123');

routerEvents.next(new NavigationEnd(1, '/share/abc123', '/share/abc123'));

expect(service.currentShareToken).toBe('abc123');
expect(service.isUnlistedShareSync()).toBeTrue();
});
});
});
41 changes: 40 additions & 1 deletion src/app/share-links/services/share-links.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Injectable } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { filter } from 'rxjs/operators';
import { ShareLink } from '../models/share-link';
import { ShareLinksApiService } from './share-links-api.service';

Expand All @@ -9,16 +11,53 @@ export class ShareLinksService {
private _currentShareToken = '';
private _shareLinks: ShareLink[] = undefined;

constructor(private shareLinksApiService: ShareLinksApiService) {}
constructor(
private shareLinksApiService: ShareLinksApiService,
private router: Router,
) {
this.router.events
.pipe(filter((event) => event instanceof NavigationEnd))
.subscribe((event: NavigationEnd) => {
if (!event.urlAfterRedirects.startsWith('/share/')) {
this._currentShareToken = '';
this._shareLinks = undefined;
}
});
}

public get currentShareToken() {
return this._currentShareToken;
}

public set currentShareToken(token: string) {
if (this._currentShareToken !== token) {
this._shareLinks = undefined;
}
this._currentShareToken = token;
}

public async primeForToken(token: string): Promise<void> {
this.currentShareToken = token;
if (!token) {
this._shareLinks = [];
return;
}
try {
this._shareLinks = await this.shareLinksApiService.getShareLinksByToken([
token,
]);
} catch {
this._shareLinks = [];
}
}

public isUnlistedShareSync(): boolean {
if (!this._currentShareToken || !this._shareLinks?.length) {
return false;
}
return this._shareLinks[0].accessRestrictions === 'none';
}

public async isUnlistedShare() {
if (!this._currentShareToken) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { AccountVO, ArchiveVO, RecordVO } from '@root/app/models';
import { AuthResponse } from '@shared/services/api/auth.repo';
import { Subject } from 'rxjs';
import { ShareLinksService } from '@root/app/share-links/services/share-links.service';
import { AccountService } from '@shared/services/account/account.service';
import { ApiService } from '@shared/services/api/api.service';
import { GoogleAnalyticsService } from '@shared/services/google-analytics/google-analytics.service';
import { ShareResponse } from '@shared/services/api/share.repo';
Expand Down Expand Up @@ -72,6 +73,7 @@ mockAccountService.accountChange = new Subject<AccountVO>();
const mockShareLinksService = {
currentShareToken: null,
isUnlistedShare: () => true,
isUnlistedShareSync: () => true,
};

const mockFilesystemService = {
Expand Down Expand Up @@ -123,6 +125,11 @@ describe('SharePreviewComponent', () => {
useValue: mockRoute,
});

config.providers.push({
provide: AccountService,
useValue: mockAccountService,
});

config.providers.push({
provide: ShareLinksService,
useValue: mockShareLinksService,
Expand Down Expand Up @@ -159,15 +166,9 @@ describe('SharePreviewComponent', () => {
expect(component).toBeTruthy();
});

it('should mark it as unlisted share if restrictions are none', fakeAsync(() => {
spyOn(mockShareLinksService, 'isUnlistedShare').and.returnValue(true);
component.ngOnInit();

expect(mockShareLinksService.isUnlistedShare).toHaveBeenCalled();
tick(1005);

it('should mark it as unlisted share synchronously when restrictions are none', () => {
expect(component.isUnlistedShare).toEqual(true);
}));
});

it('should open dialog shortly after loading if user is logged out and it is not an unlisted share', fakeAsync(() => {
const dialogRefSpy = jasmine.createSpyObj('DialogRef', ['close']);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ export class SharePreviewComponent implements OnInit, OnDestroy {
private dataService: DataService,
) {
this.shareToken = this.route.snapshot.params.shareToken;
this.isUnlistedShare = this.shareLinksService.isUnlistedShareSync();
this.ephemeralFolder = this.route.snapshot.data.ephemeralFolder ?? null;

this.signupForm = fb.group({
invitation: [this.isInvite ? this.sharePreviewVO.token : ''],
Expand Down Expand Up @@ -192,19 +194,7 @@ export class SharePreviewComponent implements OnInit, OnDestroy {
}

async ngOnInit() {
this.shareLinksService.currentShareToken = this.shareToken;
this.isUnlistedShare = await this.shareLinksService.isUnlistedShare();

if (this.isUnlistedShare) {
if (this.route.snapshot.data.sharePreviewVO?.FolderVO) {
this.ephemeralFolder = await this.filesystemService.getFolder(
this.route.snapshot.data.sharePreviewVO.FolderVO,
);
} else {
this.ephemeralFolder = this.route.snapshot.data.currentFolder;
this.ephemeralFolder.folderId =
this.route.snapshot.data.sharePreviewVO?.RecordVO?.parentFolderId;
}
if (this.ephemeralFolder) {
this.dataService.ephemeralFolder = this.ephemeralFolder;
this.dataService.pushBreadcrumbFolder(this.ephemeralFolder);
}
Expand Down Expand Up @@ -253,8 +243,6 @@ export class SharePreviewComponent implements OnInit, OnDestroy {
}

ngOnDestroy(): void {
this.shareLinksService.currentShareToken = undefined;

this.routerListener.unsubscribe();
this.accountListener.unsubscribe();
this.archiveListener.unsubscribe();
Expand Down
Loading