diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index fb537c449cfef0869fef975766f58a14f5876598..4bae76e250156515cc188c99f976a4e50bdd2ef5 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts @@ -9,6 +9,7 @@ import { UserProfileComponent } from './users/user-profile/user-profile.componen const routes: Routes = [ { path: 'annonse/ny', component: PostFormComponent }, + { path: 'annonse/rediger/:id', component: PostFormComponent }, { path: 'annonse', component: PostListComponent }, { path: 'annonse/:id', component: PostDetailsComponent }, diff --git a/client/src/app/posts/post-details/post-details.component.html b/client/src/app/posts/post-details/post-details.component.html index 82d2c974c54aecb3fb5fa36438205f2b5edb0f36..381d4869dca543fe9b80cee79e9569fda78da342 100644 --- a/client/src/app/posts/post-details/post-details.component.html +++ b/client/src/app/posts/post-details/post-details.component.html @@ -5,3 +5,6 @@ <br> <p>Publiseringstidspunkt: {{post.getTimestamp}}</p> <p>Eier: {{post.getOwner}}</p> + +<app-button text="Rediger annonse" (click)="editPost()"></app-button> +<app-button text="Slett annonse" (click)="deletePost()"></app-button> diff --git a/client/src/app/posts/post-details/post-details.component.spec.ts b/client/src/app/posts/post-details/post-details.component.spec.ts index 90f92b94c0e46f064508801e1d57403a5fb1c26d..42603d5568ce4350023a4129d448992301039b81 100644 --- a/client/src/app/posts/post-details/post-details.component.spec.ts +++ b/client/src/app/posts/post-details/post-details.component.spec.ts @@ -1,14 +1,49 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Post } from 'src/app/models/post.model'; +import { PostService } from '../post.service'; import { PostDetailsComponent } from './post-details.component'; describe('PostDetailsComponent', () => { let component: PostDetailsComponent; let fixture: ComponentFixture<PostDetailsComponent>; + let mockPostService; beforeEach(async () => { + // PostService mock setup + mockPostService = jasmine.createSpyObj(['getPost', 'deletePost']); + mockPostService.getPost.and.returnValue( + new Promise<Post>( + (resolve) => { + resolve(new Post({ + id: 5, + title: "Test", + description: "TestDescription", + timestamp: 23947298, + owner: "user", + imageUrl: null, + price: 49, + categoryid: 2 + })); + }) + ); + mockPostService.deletePost.and.returnValue( + new Promise<any>( + (resolve) => { + resolve({data: []}) + }) + ); + await TestBed.configureTestingModule({ - declarations: [ PostDetailsComponent ] + declarations: [ PostDetailsComponent ], + imports: [ HttpClientTestingModule, RouterTestingModule ], + providers: [ + { provide: ActivatedRoute, useValue: { snapshot: {params: {id: 5}}}}, + { provide: PostService, useValue: mockPostService } + ] }) .compileComponents(); }); @@ -22,4 +57,29 @@ describe('PostDetailsComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should get post with id from url parameter', async () => { + // Waits for ngOnInit and checks that we get post + fixture.whenStable().then(() => { + expect(mockPostService.getPost).toHaveBeenCalledWith(5); + expect(component.post).toEqual(new Post({ + id: 5, + title: "Test", + description: "TestDescription", + timestamp: 23947298, + owner: "user", + imageUrl: null, + price: 49, + categoryid: 2 + })); + }); + }); + + it('should delete post with id', async () => { + // Waits for ngOnInit and checks that we can delete post + fixture.whenStable().then(() => { + component.deletePost(); + expect(mockPostService.deletePost).toHaveBeenCalledWith(5); + }); + }); }); diff --git a/client/src/app/posts/post-details/post-details.component.ts b/client/src/app/posts/post-details/post-details.component.ts index 72dd9a89f30421c3ba8c25a6a8feeeb687ee87e6..3de9e4d32cfdba4092e59ac8427030c074166eca 100644 --- a/client/src/app/posts/post-details/post-details.component.ts +++ b/client/src/app/posts/post-details/post-details.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { Post } from 'src/app/models/post.model'; import { PostService } from '../post.service'; -import { ActivatedRoute } from '@angular/router' +import { ActivatedRoute, Router } from '@angular/router' @Component({ selector: 'app-post-details', @@ -12,19 +12,35 @@ export class PostDetailsComponent implements OnInit { post: Post = new Post(); - constructor(private postService: PostService, private activatedRoute: ActivatedRoute) { } + constructor(private postService: PostService, private activatedRoute: ActivatedRoute, private router: Router) { } ngOnInit(): void { // Gets id parameter from URL - this.activatedRoute.params.subscribe(params => { - const id = params["id"]; + const id = this.activatedRoute.snapshot.params["id"]; - // Gets Post with id from database - this.postService.getPost(id).then(post => { - this.post = post; - }).catch(error => { - console.log(error); - }); + // Gets Post with id from database + this.postService.getPost(id).then(post => { + this.post = post; + }).catch(error => { + console.log(error); + }); + } + /** + * Moves to edit page + */ + editPost() { + this.router.navigateByUrl("/annonse/rediger/" + this.post.getId); + } + + /** + * Deletes post in database and navigates to post list + */ + deletePost() { + this.postService.deletePost(this.post.getId).then(data => { + console.log("Successfully deleted post: " + this.post.getId); + this.router.navigateByUrl("/annonse"); + }).catch(error => { + console.log(error); }); } } diff --git a/client/src/app/posts/post-form/post-form.component.html b/client/src/app/posts/post-form/post-form.component.html index 0ea8224c49c7c4c83cf7cfda58a8dc9fecf79b2a..139ceaca43fc475a3440e5e1c01052a22c66d74f 100644 --- a/client/src/app/posts/post-form/post-form.component.html +++ b/client/src/app/posts/post-form/post-form.component.html @@ -1,5 +1,5 @@ <div class="postform"> - <h3>Lag annonse</h3> + <h3>{{id ? 'Rediger annonse' : 'Lag annonse'}}</h3> <app-text-input [(inputModel)]="title" label="Tittel" (blur)="checkForm()"></app-text-input> @@ -8,8 +8,8 @@ <app-number-input [(inputModel)]="price" label="Pris" (blur)="checkForm()"></app-number-input> <app-select [(inputModel)]="categoryid" (change)="checkForm()" label="Kategori"> - <option value="0" selected hidden>Velg kategori</option> - <option *ngFor="let category of categories" [value]="category.getCategoryId">{{category.getName}}</option> + <option value="0" [selected]="categoryid == 0" hidden>Velg kategori</option> + <option *ngFor="let category of categories" [value]="category.getCategoryId" [selected]="categoryid == category.getCategoryId">{{category.getName}}</option> </app-select> <app-text-input [(inputModel)]="imageUrl" label="Bilde URL" (blur)="showImage(imageUrl)"></app-text-input> @@ -18,4 +18,5 @@ <p>{{statusMessage}}</p> <app-button (click)="publishPost()" text="Publiser"></app-button> + <app-button *ngIf="id" (click)="deletePost()" text="Slett annonse"></app-button> </div> diff --git a/client/src/app/posts/post-form/post-form.component.spec.ts b/client/src/app/posts/post-form/post-form.component.spec.ts index 31c4d5b8657d2bc3ea4af3df65d67e7624b33d83..ac94cf73d7f117ce4d9799466e22a58ec224a381 100644 --- a/client/src/app/posts/post-form/post-form.component.spec.ts +++ b/client/src/app/posts/post-form/post-form.component.spec.ts @@ -1,5 +1,5 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; import { FormsModule } from '@angular/forms'; import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; @@ -17,7 +17,7 @@ describe('PostFormComponent', () => { beforeEach(async () => { // PostService mock setup - mockPostService = jasmine.createSpyObj(['getAllCategories', 'addPost']); + mockPostService = jasmine.createSpyObj(['getAllCategories', 'addPost', 'deletePost']); mockPostService.getAllCategories.and.returnValue( new Promise<Array<Category>>( (resolve) => { @@ -30,6 +30,12 @@ describe('PostFormComponent', () => { resolve("success") }) ); + mockPostService.deletePost.and.returnValue( + new Promise<any>( + (resolve) => { + resolve({data: []}) + }) + ); await TestBed.configureTestingModule({ declarations: [ PostFormComponent ], @@ -109,4 +115,22 @@ describe('PostFormComponent', () => { component.showImage("test"); expect(component.displayImageUrl).toBe("test"); }); + + it('should delete post with id', async () => { + component.id = 5; + + // Waits for ngOnInit and checks that we can delete post + fixture.whenStable().then(() => { + component.deletePost(); + expect(mockPostService.deletePost).toHaveBeenCalledWith(5); + }); + }); + + it('should not delete new post', async () => { + // Waits for ngOnInit and checks that we can delete post + fixture.whenStable().then(() => { + component.deletePost(); + expect(mockPostService.deletePost).not.toHaveBeenCalledWith(5); + }); + }); }); diff --git a/client/src/app/posts/post-form/post-form.component.ts b/client/src/app/posts/post-form/post-form.component.ts index 14be51a44f2ec8bb6ae959d52e81e657037a65db..4fcf05a15115a372616c5ebe720264b1ff6620b7 100644 --- a/client/src/app/posts/post-form/post-form.component.ts +++ b/client/src/app/posts/post-form/post-form.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { Category } from 'src/app/models/category.model'; import { Post } from 'src/app/models/post.model'; import { PostService } from '../post.service'; @@ -11,6 +11,8 @@ import { PostService } from '../post.service'; }) export class PostFormComponent implements OnInit { + id = 0; + title: string = ""; description: string = ""; price: number = 0; @@ -22,9 +24,27 @@ export class PostFormComponent implements OnInit { categories: Array<Category>; - constructor(private postService: PostService, private router: Router) { } + constructor(private postService: PostService, private router: Router, private activatedRoute: ActivatedRoute) { } ngOnInit() { + const id = this.activatedRoute.snapshot.params["id"]; + if (id) { + this.id = id; + + // Gets Post with id from database + this.postService.getPost(id).then(post => { + this.title = post.getTitle; + this.description = post.getDescription; + this.price = post.getPrice; + this.categoryid = post.getCategory; + this.imageUrl = post.getImageUrl; + + this.showImage(this.imageUrl); + }).catch(error => { + console.log(error); + }); + } + // Gets all categories and displays them in dropdown this.postService.getAllCategories().then(categories => { this.categories = categories; @@ -77,12 +97,37 @@ export class PostFormComponent implements OnInit { categoryid: this.categoryid }); - // Adds post to database and changes page afterwards - this.postService.addPost(newPost).then(status => { - console.log("Post was added: " + status); - this.router.navigateByUrl("/"); + if (this.id) { + // Updates post with id in database and changes page afterwards + this.postService.updatePost(this.id, newPost).then(status => { + console.log("Post was added: " + status); + this.router.navigateByUrl("/annonse"); + }).catch(error => { + console.log("Error adding post: " + error); + }); + } else { + // Adds post to database and changes page afterwards + this.postService.addPost(newPost).then(status => { + console.log("Post was added: " + status); + this.router.navigateByUrl("/annonse"); + }).catch(error => { + console.log("Error adding post: " + error); + }); + } + } + } + + /** + * Deletes post in database and navigates to post list. + * Post can only be deleted if we are updating, not adding. + */ + deletePost() { + if (this.id) { + this.postService.deletePost(this.id).then(data => { + console.log("Successfully deleted post: " + this.id); + this.router.navigateByUrl("/annonse"); }).catch(error => { - console.log("Error adding post: " + error); + console.log(error); }); } } diff --git a/client/src/app/posts/post-list/post-list.component.spec.ts b/client/src/app/posts/post-list/post-list.component.spec.ts index 2d39c24944619ae92c499e826d476de93f005a4c..464c09fe2d5d9261f2ae08d144b4a0c5063c314c 100644 --- a/client/src/app/posts/post-list/post-list.component.spec.ts +++ b/client/src/app/posts/post-list/post-list.component.spec.ts @@ -1,3 +1,4 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { PostListComponent } from './post-list.component'; @@ -8,7 +9,8 @@ describe('PostListComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ PostListComponent ] + declarations: [ PostListComponent ], + imports: [ HttpClientTestingModule ] }) .compileComponents(); }); diff --git a/client/src/app/posts/post-thumbnail/post-thumbnail.component.spec.ts b/client/src/app/posts/post-thumbnail/post-thumbnail.component.spec.ts index ea7282db0c1f44a777d0bbf58a7ca927de9d6fd7..04d5c006c2140f6b05b4069b18dfac01c71f285d 100644 --- a/client/src/app/posts/post-thumbnail/post-thumbnail.component.spec.ts +++ b/client/src/app/posts/post-thumbnail/post-thumbnail.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; import { PostThumbnailComponent } from './post-thumbnail.component'; @@ -8,7 +9,8 @@ describe('PostThumbnailComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ PostThumbnailComponent ] + declarations: [ PostThumbnailComponent ], + imports: [ RouterTestingModule ] }) .compileComponents(); }); diff --git a/client/src/app/posts/post.service.spec.ts b/client/src/app/posts/post.service.spec.ts index 0439fbe95c5eafe960d20c4e678c5672ab71c278..eaf36787ea6d30889a5b08e9927672306d5f613a 100644 --- a/client/src/app/posts/post.service.spec.ts +++ b/client/src/app/posts/post.service.spec.ts @@ -176,5 +176,89 @@ describe('PostService', () => { req.error(new ErrorEvent("400")); }); }); + + describe('deletePost', () => { + it('should delete post', () => { + + // Deletes post with id = 2 + service.deletePost(2) + .then(data => {}) + .catch(error => { + fail(); + }); + + // Mocks and checks HTTP request + const req = httpMock.expectOne("api/post/2"); + expect(req.request.method).toBe("DELETE"); + req.flush({ + data: [] + }); + }); + + it('should reject on http error', () => { + + // Deletes post with id = 5, but should catch HTTP error + service.deletePost(5).then(data => { + fail(); + }).catch(error => {}); + + // Mocks and checks HTTP request + const req = httpMock.expectOne("api/post/5"); + expect(req.request.method).toBe("DELETE"); + req.error(new ErrorEvent("400")); + }); + }); + + describe('updatePost', () => { + it('should update post', () => { + let post = new Post({ + id: 2, + title: "Test", + description: "TestDescription", + timestamp: 23947298, + owner: "user", + imageUrl: null, + price: 49, + categoryid: 2 + }); + + // Updates post with id = 2 + service.updatePost(2, post) + .then(data => {}) + .catch(error => { + fail(); + }); + + // Mocks and checks HTTP request + const req = httpMock.expectOne("api/post/2"); + expect(req.request.method).toBe("PUT"); + req.flush({ + data: [] + }); + }); + + it('should reject on http error', () => { + let post = new Post({ + id: 2, + title: "Test", + description: "TestDescription", + timestamp: 23947298, + owner: "user", + imageUrl: null, + price: 49, + categoryid: 2 + }); + + // Updates post with id = 2, but should catch HTTP error + service.updatePost(2, post).then(data => { + fail(); + }).catch(error => {}); + + // Mocks and checks HTTP request + const req = httpMock.expectOne("api/post/2"); + expect(req.request.method).toBe("PUT"); + req.error(new ErrorEvent("400")); + }); + }); }); diff --git a/client/src/app/posts/post.service.ts b/client/src/app/posts/post.service.ts index e28949586aaf3f30b7c0a8441aae4cc882aa2374..3a09b533c6da23da313072008c1dc922976f133e 100644 --- a/client/src/app/posts/post.service.ts +++ b/client/src/app/posts/post.service.ts @@ -11,6 +11,8 @@ export class PostService { postUrl = "api/post/"; categoryUrl = "api/category/"; + categories: Array<Category>; + constructor(private http: HttpClient) { } /** @@ -110,6 +112,11 @@ export class PostService { getAllCategories(): Promise<Array<Category>>{ return new Promise<Array<Category>>( (resolve, reject) => { + if (this.categories) { + resolve(this.categories); + return; + } + this.get_all_categories().subscribe((data: any) => { try { let outputCategories = []; @@ -122,6 +129,8 @@ export class PostService { return; } } + + this.categories = outputCategories; resolve(outputCategories); } catch (err: any){ reject(err); @@ -138,4 +147,54 @@ export class PostService { private get_all_categories() { return this.http.get(this.categoryUrl); } + + /** + * Delete post in database by id. + */ + deletePost(id: number): Promise<any> { + return new Promise<any>( + (resolve, reject) => { + this.delete_post(id).subscribe((data: any) => { + try { + resolve(data); + } catch (err: any) { + reject(err); + } + }, + (err: any) => { + console.log(err.message); + reject(err); + }); + } + ); + } + + private delete_post(id: number) { + return this.http.delete(this.postUrl + id); + } + + /** + * Update post in database by id. + */ + updatePost(id: number, post: Post): Promise<any> { + return new Promise<any>( + (resolve, reject) => { + this.update_post(id, post).subscribe((data: any) => { + try { + resolve(data); + } catch (err: any) { + reject(err); + } + }, + (err: any) => { + console.log(err.message); + reject(err); + }); + } + ); + } + + private update_post(id: number, post: Post) { + return this.http.put(this.postUrl + id, post.serialize()); + } } diff --git a/server/src/controllers/postController/index.ts b/server/src/controllers/postController/index.ts index 7fcf43dcf493d33699e30fc07507c4336037e1bd..4cd016268cd7f0f86e314dec0638bc8b14720331 100644 --- a/server/src/controllers/postController/index.ts +++ b/server/src/controllers/postController/index.ts @@ -14,23 +14,26 @@ router.route("/").post(async (request: Request, response: Response) => { const { title, description, + price, timestamp, owner, - category, + categoryid, imageUrl, } = request.body; try { const post: IPost = { title: title, description: description, + price: price, timestamp: timestamp, owner: owner, - categoryid: category, + categoryid: categoryid, imageUrl: imageUrl, }; + if (Object.values(post).filter((p) => p == undefined).length > 0) return response.status(500).send("Error"); - const input = `INSERT INTO post(title, description, timestamp, owner, category, imageUrl) VALUES (?,?,?,?,?,?)`; + const input = `INSERT INTO post(title, description, price, timestamp, owner, categoryid, imageUrl) VALUES (?,?,?,?,?,?,?)`; return response.status(200).json(await query(input, Object.values(post))); } catch (error) { return response.status(400).send("Bad Request"); @@ -42,9 +45,8 @@ router.route("/").post(async (request: Request, response: Response) => { router.route("/").get(async (request: Request, response: Response) => { const { categoryid } = request.query as { [key: string]: string }; try { - let input = `SELECT p.id, p.title, p.description, p.timestamp, p.owner, category.name, p.imageUrl - FROM post as p - INNER JOIN category ON category.categoryid = p.categoryid`; + let input = `SELECT p.id, p.title, p.description, p.price, p.timestamp, p.owner, p.categoryid, p.imageUrl + FROM post as p`; if (categoryid) input += ` WHERE p.categoryid=${categoryid}`; response.status(200).json(await query(input, "")); } catch (error) { @@ -56,9 +58,9 @@ router.route("/").get(async (request: Request, response: Response) => { router.route("/:id").get(async (request: Request, response: Response) => { const postId: string = request.params.id as string; try { - const input = `SELECT p.id, p.title, p.description, p.timestamp, p.owner, category.name, p.imageUrl + const input = `SELECT p.id, p.title, p.description, p.price, p.timestamp, p.owner, p.categoryid, p.imageUrl FROM post as p - INNER JOIN category ON category.categoryid = p.categoryid WHERE p.id=?;`; + WHERE p.id=?;`; response.status(200).json(await query(input, [postId])); } catch (error) { response.status(400).send("Bad Request"); @@ -69,10 +71,29 @@ router.route("/:id").get(async (request: Request, response: Response) => { // Edit post with id `/api/post/:id` router.route("/:id").put(async (request: Request, response: Response) => { const postId: string = request.params.id as string; + const { + title, + description, + price, + timestamp, + owner, + categoryid, + imageUrl, + } = request.body; try { + const post: IPost = { + title: title, + description: description, + price: price, + timestamp: timestamp, + owner: owner, + categoryid: categoryid, + imageUrl: imageUrl, + }; + response .status(200) - .json(await query("SELECT * FROM post WHERE id=?;", [postId])); + .json(await query("UPDATE post SET title=?, description=?, price=?, timestamp=?, categoryid=?, imageUrl=? WHERE id=?;", [title, description, price, timestamp, categoryid, imageUrl, postId])); } catch (error) { response.status(400).send("Bad Request"); } @@ -85,7 +106,7 @@ router.route("/:id").delete(async (request: Request, response: Response) => { try { response .status(200) - .json(await query("SELECT * FROM post WHERE id=?;", [postId])); + .json(await query("DELETE FROM post WHERE id=?;", [postId])); } catch (error) { response.status(400).send("Bad Request"); } diff --git a/server/src/models/post.ts b/server/src/models/post.ts index a7e5a566baf8388dd99a8a014264714219808a4c..ad301aa6174e8eda398dd9cbf3b4b539f52e060b 100644 --- a/server/src/models/post.ts +++ b/server/src/models/post.ts @@ -2,6 +2,7 @@ interface IPost { title: string; description: string; + price: string; timestamp: number; owner: string; categoryid: number;